mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Stage 1
This commit is contained in:
parent
1ca023be36
commit
3a4d8951d5
20 changed files with 1511 additions and 389 deletions
120
.skills/csharp-async-best-practices/SKILL.md
Normal file
120
.skills/csharp-async-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
name: csharp-async-best-practices
|
||||
description: Use when reviewing, writing, refactoring, or designing c# async code that uses task, task-generic, valuetask, cancellationtoken, task.whenall, task.whenany, task.run, configureawait, async void, or fire-and-forget patterns. Trigger on `.result`, `.wait()`, deadlocks, cancellation propagation, asp.net core background work, ui responsiveness, exception flow, and performance-sensitive async api design.
|
||||
metadata:
|
||||
category: technique
|
||||
triggers:
|
||||
- c#
|
||||
- async
|
||||
- task
|
||||
- valuetask
|
||||
- cancellationtoken
|
||||
- configureawait
|
||||
- .result
|
||||
- .wait()
|
||||
- async void
|
||||
- fire-and-forget
|
||||
- task.run
|
||||
- whenall
|
||||
- whenany
|
||||
- asp.net core
|
||||
- deadlock
|
||||
---
|
||||
|
||||
# C# Async Best Practices
|
||||
|
||||
## Overview
|
||||
|
||||
Apply evidence-backed async guidance with this priority order:
|
||||
|
||||
1. correctness and cancellation semantics
|
||||
2. context-specific API design
|
||||
3. concurrency behavior and failure handling
|
||||
4. performance tuning only when the hot path is real
|
||||
|
||||
Treat blanket advice as suspect. Separate official behavior from expert interpretation and from your own recommendation.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Classify the code before judging it.
|
||||
- **I/O-bound async**: network, file, database, timers, async waits
|
||||
- **CPU-bound work**: expensive computation
|
||||
- **Context**: library, UI app, ASP.NET Core app, background service, test code
|
||||
- **Pressure**: hot path or ordinary path
|
||||
2. Prefer the least surprising correct design.
|
||||
3. Only optimize allocations or scheduling after the correctness story is sound.
|
||||
4. Load the matching reference file before making strong claims.
|
||||
- **General rules and code review defaults**: `references/core-guidance.md`
|
||||
- **Context-sensitive rules**: `references/context-and-tradeoffs.md`
|
||||
- **Source notes and authority breakdown**: `references/source-notes.md`
|
||||
|
||||
## Review defaults
|
||||
|
||||
Start from these defaults unless the case-specific evidence says otherwise:
|
||||
|
||||
| Topic | Default judgment |
|
||||
|---|---|
|
||||
| Blocking on async | usually a defect or interop boundary smell |
|
||||
| `async void` | only acceptable for event handlers |
|
||||
| `ValueTask` | avoid by default; justify with measurements or a very hot path |
|
||||
| `ConfigureAwait(false)` | good library default, not an app-wide default |
|
||||
| `Task.Run` | use to offload CPU work when needed, not to fake async I/O |
|
||||
| Fire-and-forget | assume unsafe until lifecycle, scope, and exception handling are explicit |
|
||||
| `Task.WhenAll` | prefer for independent concurrent operations |
|
||||
| `Task.WhenAny` | always inspect winner and define what happens to losers |
|
||||
| Cancellation | accept and propagate token until the point of no cancellation |
|
||||
|
||||
## Output contract
|
||||
|
||||
When you review or design code, label your reasoning like this:
|
||||
|
||||
- **Fact**: official runtime or API behavior
|
||||
- **Expert guidance**: interpretation from strong experts when it adds design meaning
|
||||
- **Synthesis**: your recommendation for this exact case
|
||||
|
||||
Do not present contextual advice as a universal law.
|
||||
|
||||
## Common traps
|
||||
|
||||
- Calling `.Result`, `.Wait()`, or `GetAwaiter().GetResult()` inside normal async-capable code
|
||||
- Recommending `ConfigureAwait(false)` everywhere because “it is .NET Core” or “it prevents deadlocks”
|
||||
- Recommending `Task.Run` inside ASP.NET Core request code just to make code “more async”
|
||||
- Recommending `ValueTask` for every hot-looking method without checking completion behavior, call frequency, or single-consumer assumptions
|
||||
- Ignoring cancellation after plumbing a `CancellationToken`
|
||||
- Using `Task.WhenAny` without awaiting the returned winner task or handling the remaining tasks
|
||||
- Treating fire-and-forget as harmless when it touches scoped services, `HttpContext`, or unobserved failures
|
||||
|
||||
## Rationalization traps
|
||||
|
||||
| Rationalization | Better reasoning |
|
||||
|---|---|
|
||||
| “It works, so `.Result` is fine.” | Lack of failure under one context does not make blocking safe or scalable. |
|
||||
| “`ValueTask` is always faster.” | It trades simplicity for niche allocation wins and stricter consumption rules. |
|
||||
| “`ConfigureAwait(false)` everywhere is modern guidance.” | Library and app code have different constraints. Blanket rules are weak. |
|
||||
| “`Task.Run` makes server code asynchronous.” | It only queues work; it does not turn blocking I/O into true async I/O. |
|
||||
| “Fire-and-forget is okay because logging exists.” | Logging does not solve scope lifetime, shutdown, retries, or error propagation. |
|
||||
|
||||
## Deliverable shape
|
||||
|
||||
For code review or implementation help, prefer:
|
||||
|
||||
1. a short context classification
|
||||
2. the concrete problem
|
||||
3. the corrected pattern
|
||||
4. the context-dependent tradeoff, if any
|
||||
5. the smallest safe code change
|
||||
|
||||
## API shape and testability
|
||||
|
||||
- Prefer `Async` suffixes for awaitable-returning methods unless an established contract or event pattern dictates otherwise.
|
||||
- Prefer `Task`-returning seams over hidden background work so tests can await completion, faults, and cancellation.
|
||||
- For timers, queues, retries, or background pipelines, recommend abstractions that let tests control time and observe completion.
|
||||
- When reviewing an async API, ask whether callers can compose it, cancel it, await it, and assert its failure behavior.
|
||||
|
||||
## Hard boundaries
|
||||
|
||||
- Do not endorse sync-over-async as a normal design choice.
|
||||
- Do not suggest `async void` except for event handlers.
|
||||
- Do not suggest `ValueTask` unless the constraints are understood.
|
||||
- Do not claim `ConfigureAwait(false)` is always needed or always unnecessary.
|
||||
- Do not approve fire-and-forget unless ownership, exception handling, and lifetime are explicit.
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
description: >-
|
||||
Context-specific async guidance for library code, ui apps, asp.net core,
|
||||
background work, task.run, configureawait, and performance-sensitive design.
|
||||
metadata:
|
||||
tags: [configureawait, task.run, asp.net core, ui, library, performance]
|
||||
source: mixed
|
||||
---
|
||||
|
||||
# Context and Tradeoffs
|
||||
|
||||
## Library code versus app code
|
||||
|
||||
### General-purpose library code
|
||||
- Prefer APIs that expose true async for I/O-bound work.
|
||||
- Do not add async wrappers around purely compute-bound methods just to look modern. Expose sync compute APIs and let callers decide whether to offload.
|
||||
- `ConfigureAwait(false)` is a strong default when the library does not need the caller’s context.
|
||||
- Avoid ambient assumptions about a UI thread, request context, or test framework behavior.
|
||||
|
||||
### App code
|
||||
- Prefer the style that fits the app model.
|
||||
- UI code often needs the original context after `await`.
|
||||
- ASP.NET Core request code normally does not need `Task.Run` just to stay responsive, because it already runs on thread pool threads.
|
||||
- Do not present “ASP.NET Core has no synchronization context” as proof that every `ConfigureAwait(false)` discussion is obsolete.
|
||||
|
||||
## `Task.Run` boundaries
|
||||
|
||||
### Good uses
|
||||
- Offload CPU-bound work so a UI thread can stay responsive.
|
||||
- Offload CPU work from a caller when that scheduling boundary is deliberate.
|
||||
|
||||
### Weak uses
|
||||
- Wrapping synchronous I/O to pretend it is true async I/O.
|
||||
- Calling `Task.Run` and immediately awaiting it in ASP.NET Core request handling when no CPU offload goal exists.
|
||||
- Using `Task.Run` to hide blocking APIs instead of fixing the underlying API choice.
|
||||
|
||||
## Fire-and-forget
|
||||
|
||||
### Assume unsafe until proven otherwise
|
||||
A background task needs answers for all of these:
|
||||
- Who owns its lifetime?
|
||||
- How are exceptions observed?
|
||||
- How does shutdown cancel it?
|
||||
- Does it touch scoped services or request-bound objects?
|
||||
- Does work need retries, backpressure, or queueing?
|
||||
|
||||
### Safer alternatives
|
||||
- Await the task normally.
|
||||
- Queue work to an owned background component.
|
||||
- In ASP.NET Core, prefer hosted services or a dedicated background queue pattern for long-lived work.
|
||||
- If scoped services are required in background processing, create an explicit scope instead of capturing request scope objects.
|
||||
|
||||
## `ConfigureAwait`
|
||||
|
||||
### Strong recommendation
|
||||
- In general-purpose libraries, use `ConfigureAwait(false)` unless the continuation must run in the captured context.
|
||||
|
||||
### Weak recommendation
|
||||
- “Always use it in app code.”
|
||||
- “Never use it on .NET Core.”
|
||||
- “Use it once at the first await and you are done.”
|
||||
|
||||
### Review note
|
||||
If code after the `await` needs a specific context, say so explicitly. If it does not, the recommendation depends on whether the code is app-level or general-purpose library code.
|
||||
|
||||
## Performance guidance
|
||||
|
||||
### Correctness first
|
||||
Do not trade API clarity for speculative micro-optimizations.
|
||||
|
||||
### `ValueTask` is performance-specialized
|
||||
Recommend it only when most of these are true:
|
||||
1. the method is called very frequently
|
||||
2. it often completes synchronously or from a reusable source
|
||||
3. allocation reduction matters on measurements
|
||||
4. consumers can respect single-consumer semantics
|
||||
5. task combinator ergonomics are not central to the API
|
||||
|
||||
### Throttling and concurrency control
|
||||
- `Task.WhenAll` expresses concurrency; it does not limit it.
|
||||
- For bounded concurrency, use an async gate such as `SemaphoreSlim.WaitAsync`, or platform helpers such as `Parallel.ForEachAsync` when the workload fits.
|
||||
- Always define what happens to remaining work after the first completion or first failure.
|
||||
105
.skills/csharp-async-best-practices/references/core-guidance.md
Normal file
105
.skills/csharp-async-best-practices/references/core-guidance.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
description: >-
|
||||
Source-backed core guidance for task, valuetask, cancellation, exception flow,
|
||||
blocking, and concurrency in c# async code reviews and implementations.
|
||||
metadata:
|
||||
tags: [csharp, async, task, valuetask, cancellation, exceptions, concurrency]
|
||||
source: mixed
|
||||
---
|
||||
|
||||
# Core Guidance
|
||||
|
||||
## Facts from official .NET documentation
|
||||
|
||||
### 1. Return types and `async void`
|
||||
- Async methods should normally return `Task` or `Task<T>`.
|
||||
- `async void` is intended for event handlers; callers cannot await it and exception handling differs.
|
||||
- TAP methods that return awaitable types conventionally use the `Async` suffix.
|
||||
|
||||
### 2. Blocking on async
|
||||
- `Task<T>.Result` is blocking. Prefer `await` in most cases.
|
||||
- Blocking can deadlock in context-bound environments and reduces scalability even when it does not deadlock.
|
||||
- `await` on a faulted task rethrows one exception directly; `.Wait()` and `.Result` wrap failures in `AggregateException`.
|
||||
|
||||
### 3. `Task` versus `ValueTask`
|
||||
- Default to `Task` or `Task<T>` unless there is a demonstrated reason not to.
|
||||
- `ValueTask` has stricter usage rules. A given instance should generally be awaited only once.
|
||||
- Do not await the same `ValueTask` multiple times, call `AsTask()` multiple times, or mix consumption techniques on the same instance.
|
||||
- For synchronously successful `Task`-returning methods, `Task.CompletedTask` is the normal zero-result completion value.
|
||||
|
||||
### 4. Cancellation
|
||||
- If a TAP method supports cancellation, expose a `CancellationToken`.
|
||||
- Pass the token to nested operations that should participate in cancellation.
|
||||
- If an async method throws `OperationCanceledException` associated with the method’s token, the returned task transitions to `Canceled`.
|
||||
- After a method has completed its work successfully, do not report cancellation instead of success.
|
||||
|
||||
### 5. Exception flow and task combinators
|
||||
- `Task.WhenAll` does not block the calling thread.
|
||||
- If any supplied task faults, the `WhenAll` task faults and aggregates the unwrapped exceptions from the component tasks.
|
||||
- If none fault and at least one is canceled, the `WhenAll` task is canceled.
|
||||
- `Task.WhenAny` returns a task that completes successfully with the first completed task as its result, even when that winning task itself is faulted or canceled.
|
||||
- After `WhenAny`, await the returned winner task to propagate its outcome.
|
||||
- The remaining tasks continue unless you cancel or otherwise handle them.
|
||||
|
||||
## Expert guidance that is strong and technically grounded
|
||||
|
||||
### Stephen Toub
|
||||
- Use `ConfigureAwait(false)` as the general default for general-purpose library code, because library code should not depend on an app model’s context.
|
||||
- App-level code is different. UI code often needs the captured context. ASP.NET Core also changes the deadlock discussion because it does not install the classic ASP.NET style synchronization context, but that does not make blanket `ConfigureAwait` advice strong.
|
||||
- `ValueTask<T>` exists mainly to avoid allocations on frequently synchronous success paths. It is not a general replacement for `Task<T>` because `Task` is more flexible for multiple awaits, caching, and combinators.
|
||||
|
||||
### Andrew Arnott
|
||||
- Propagate the token until the point of no cancellation.
|
||||
- Validate arguments before cancellation checks when argument validation should always run.
|
||||
- Prefer catching `OperationCanceledException` rather than `TaskCanceledException` in general-purpose logic.
|
||||
- Keep `CancellationToken` last in the parameter list; make it optional mainly on public APIs, not necessarily on internal methods.
|
||||
|
||||
### Stephen Cleary
|
||||
- “Async all the way” is a strong design guideline, not an absolute law of physics. Sync bridges exist, but they are specialized boundary decisions, not a normal code review recommendation.
|
||||
- `async void` and sync-over-async both create real observability and composition problems even when a sample appears to work.
|
||||
|
||||
## Naming and testability
|
||||
|
||||
### Naming
|
||||
- TAP methods that return awaitable types conventionally use the `Async` suffix. Do not force renames when an interface, base class, or event pattern already dictates the name.
|
||||
|
||||
### Testability
|
||||
- Favor awaitable APIs over hidden work so tests can await completion, assert faults, and drive cancellation deterministically.
|
||||
- Prefer explicit background components, injected clocks, and owned queues over ad hoc fire-and-forget logic that tests cannot observe.
|
||||
|
||||
## Synthesis for agents
|
||||
|
||||
### Code review defaults
|
||||
- Treat `.Result`, `.Wait()`, and `GetAwaiter().GetResult()` as likely defects unless the code is a deliberate sync boundary and the caller explicitly cannot be async.
|
||||
- Prefer `Task`/`Task<T>` for API design. Require an explicit reason before recommending `ValueTask`.
|
||||
- Require cancellation behavior to be coherent: accepted, propagated, and not silently dropped.
|
||||
- Prefer `await Task.WhenAll(...)` for independent operations started before awaiting.
|
||||
- Treat `Task.WhenAny(...)` as incomplete until the winner is awaited and losers are canceled, observed, or intentionally left running.
|
||||
|
||||
### Minimal examples
|
||||
|
||||
#### Avoid sync-over-async
|
||||
```csharp
|
||||
// bad
|
||||
var user = client.GetUserAsync(id).Result;
|
||||
|
||||
// better
|
||||
var user = await client.GetUserAsync(id);
|
||||
```
|
||||
|
||||
#### Use `Task.WhenAll` for parallel I/O
|
||||
```csharp
|
||||
var userTask = repo.GetUserAsync(id, ct);
|
||||
var ordersTask = repo.GetOrdersAsync(id, ct);
|
||||
await Task.WhenAll(userTask, ordersTask);
|
||||
return new Dashboard(await userTask, await ordersTask);
|
||||
```
|
||||
|
||||
#### Be conservative with `ValueTask`
|
||||
```csharp
|
||||
// default
|
||||
Task<Item?> GetAsync(string key, CancellationToken ct);
|
||||
|
||||
// specialized hot path only when justified
|
||||
ValueTask<Item?> TryGetCachedAsync(string key);
|
||||
```
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
description: >-
|
||||
Authority notes and citations for the c# async best practices skill, separating
|
||||
official documentation, expert interpretation, and synthesized guidance.
|
||||
metadata:
|
||||
tags: [sources, citations, authority, notes]
|
||||
source: external
|
||||
---
|
||||
|
||||
# Source Notes
|
||||
|
||||
## Official facts
|
||||
|
||||
- Microsoft Learn, "Implementing the Task-based Asynchronous Pattern"
|
||||
- https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/implementing-the-task-based-asynchronous-pattern
|
||||
- Return types, cancellation behavior, `Task.Run` boundaries, and TAP implementation guidance.
|
||||
- Microsoft Learn, "Consuming the Task-based Asynchronous Pattern"
|
||||
- https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern
|
||||
- `await`, `WhenAll`, `WhenAny`, cancellation propagation, and exception behavior.
|
||||
- Microsoft Learn, "Async return types"
|
||||
- https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-return-types
|
||||
- `Task`, `Task<T>`, `async void`, generalized async return types.
|
||||
- Microsoft Learn, `ValueTask` API reference
|
||||
- https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask
|
||||
- single-consumer warnings and default-to-`Task` guidance.
|
||||
- Microsoft Learn, ASP.NET Core best practices
|
||||
- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices
|
||||
- avoid blocking calls, avoid unnecessary `Task.Run`, background-work cautions.
|
||||
- Microsoft Learn, hosted services in ASP.NET Core
|
||||
- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services
|
||||
- safe long-lived background work and cancellation during shutdown.
|
||||
|
||||
## Expert guidance used only when technically grounded
|
||||
|
||||
- Stephen Toub, ".NET Blog: ConfigureAwait FAQ"
|
||||
- https://devblogs.microsoft.com/dotnet/configureawait-faq/
|
||||
- best source for context capture semantics and library-vs-app guidance.
|
||||
- Stephen Toub, ".NET Blog: Understanding the Whys, Whats, and Whens of ValueTask"
|
||||
- https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/
|
||||
- performance rationale and tradeoffs behind `ValueTask<T>`.
|
||||
- Stephen Toub, ".NET Blog: Await, and UI, and deadlocks! Oh my!"
|
||||
- https://devblogs.microsoft.com/dotnet/await-and-ui-and-deadlocks-oh-my/
|
||||
- canonical deadlock explanation for context-bound code.
|
||||
- Stephen Toub, ".NET Blog: Task Exception Handling in .NET 4.5"
|
||||
- https://devblogs.microsoft.com/dotnet/task-exception-handling-in-net-4-5/
|
||||
- explains `await` versus blocking exception shape and why `WhenAll` matters.
|
||||
- Andrew Arnott, "Recommended patterns for CancellationToken"
|
||||
- https://devblogs.microsoft.com/premier-developer/recommended-patterns-for-cancellationtoken/
|
||||
- practical cancellation design heuristics; useful, but not treated as a language/runtime spec.
|
||||
- Stephen Cleary, "Async/Await - Best Practices in Asynchronous Programming"
|
||||
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
|
||||
- useful design interpretation, but older and treated as contextual guidance rather than current official policy.
|
||||
|
||||
## Where the skill is intentionally cautious
|
||||
|
||||
- `ConfigureAwait`: strong guidance exists for libraries, weaker guidance for app code. Blanket rules are rejected.
|
||||
- `Task.Run`: valid for deliberate CPU offload, weak as a server-side patch for blocking I/O.
|
||||
- `ValueTask`: supported and useful, but easy to misuse. The skill defaults to `Task` unless evidence is present.
|
||||
- Fire-and-forget: acceptable only with explicit ownership and lifecycle design, especially in server code.
|
||||
|
|
@ -233,84 +233,120 @@ namespace MinecraftClient
|
|||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var cts = new CancellationTokenSource();
|
||||
_cancellationTokenSource = cts;
|
||||
var previousTask = _latestTask;
|
||||
var newTask = new Task(async () =>
|
||||
Task newTask = UpdateSuggestionsAsync(fullCommand, offset, buffer.CursorPosition, cts.Token);
|
||||
_latestTask = newTask;
|
||||
_ = ObserveAutocompleteTaskAsync(newTask, cts);
|
||||
}
|
||||
else
|
||||
{
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateSuggestionsAsync(string fullCommand, int offset, int cursorPosition, CancellationToken cancellationToken)
|
||||
{
|
||||
string command = fullCommand[offset..];
|
||||
if (command.Length == 0)
|
||||
{
|
||||
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new();
|
||||
|
||||
sugList.Add(new("/"));
|
||||
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> suggestionList = new()
|
||||
{
|
||||
new("/")
|
||||
};
|
||||
|
||||
var childs = McClient.dispatcher.GetRoot().Children;
|
||||
if (childs is not null)
|
||||
{
|
||||
foreach (var child in childs)
|
||||
sugList.Add(new(child.Name));
|
||||
suggestionList.Add(new(child.Name));
|
||||
}
|
||||
|
||||
foreach (var cmd in Commands)
|
||||
sugList.Add(new(cmd));
|
||||
suggestionList.Add(new(cmd));
|
||||
|
||||
SendSuggestions(sugList.ToArray(), new(offset, offset));
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
SendSuggestions(suggestionList.ToArray(), new(offset, offset));
|
||||
return;
|
||||
}
|
||||
else if (command.Length > 0 && command[0] == '/' && !command.Contains(' '))
|
||||
|
||||
if (command[0] == '/' && !command.Contains(' '))
|
||||
{
|
||||
var sorted = Process.ExtractSorted(command[1..], Commands);
|
||||
var sugList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sorted.Count()];
|
||||
var suggestionList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sorted.Count()];
|
||||
|
||||
int index = 0;
|
||||
foreach (var sug in sorted)
|
||||
sugList[index++] = new(sug.Value);
|
||||
SendSuggestions(sugList, new(offset, offset + command.Length));
|
||||
foreach (var suggestion in sorted)
|
||||
suggestionList[index++] = new(suggestion.Value);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
SendSuggestions(suggestionList, new(offset, offset + command.Length));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
CommandDispatcher<CmdResult>? dispatcher = McClient.dispatcher;
|
||||
if (dispatcher is null)
|
||||
return;
|
||||
|
||||
ParseResults<CmdResult> parse = dispatcher.Parse(command, CmdResult.Empty);
|
||||
Brigadier.NET.Suggestion.Suggestions suggestions =
|
||||
await dispatcher.GetCompletionSuggestions(parse, cursorPosition - offset);
|
||||
|
||||
Brigadier.NET.Suggestion.Suggestions suggestions = await dispatcher.GetCompletionSuggestions(parse, buffer.CursorPosition - offset);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
int sugLen = suggestions.List.Count;
|
||||
if (sugLen == 0)
|
||||
int suggestionCount = suggestions.List.Count;
|
||||
if (suggestionCount == 0)
|
||||
{
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string?> dictionary = new();
|
||||
foreach (var sug in suggestions.List)
|
||||
dictionary.Add(sug.Text, sug.Tooltip?.String);
|
||||
|
||||
var sugList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sugLen];
|
||||
if (cts.IsCancellationRequested)
|
||||
return;
|
||||
Dictionary<string, string?> tooltips = new();
|
||||
foreach (var suggestion in suggestions.List)
|
||||
tooltips.Add(suggestion.Text, suggestion.Tooltip?.String);
|
||||
|
||||
Tuple<int, int> range = new(suggestions.Range.Start + offset, suggestions.Range.End + offset);
|
||||
var sorted = Process.ExtractSorted(fullCommand[range.Item1..range.Item2], dictionary.Keys);
|
||||
if (cts.IsCancellationRequested)
|
||||
var sortedSuggestions = Process.ExtractSorted(fullCommand[range.Item1..range.Item2], tooltips.Keys);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
int index = 0;
|
||||
foreach (var sug in sorted)
|
||||
sugList[index++] = new(sug.Value, dictionary[sug.Value] ?? string.Empty);
|
||||
var suggestionListWithTooltips = new ConsoleInteractive.ConsoleSuggestion.Suggestion[suggestionCount];
|
||||
int suggestionIndex = 0;
|
||||
foreach (var suggestion in sortedSuggestions)
|
||||
suggestionListWithTooltips[suggestionIndex++] = new(suggestion.Value, tooltips[suggestion.Value] ?? string.Empty);
|
||||
|
||||
SendSuggestions(sugList, range);
|
||||
SendSuggestions(suggestionListWithTooltips, range);
|
||||
}
|
||||
}, cts.Token);
|
||||
_latestTask = newTask;
|
||||
try { newTask.Start(); } catch { }
|
||||
if (_cancellationTokenSource == cts) _cancellationTokenSource = null;
|
||||
}
|
||||
else
|
||||
|
||||
private static async Task ObserveAutocompleteTaskAsync(Task task, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
WriteLogLine(e.ToString(), acceptnewlines: true);
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_cancellationTokenSource, cancellationTokenSource))
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient.Crypto
|
||||
{
|
||||
|
|
@ -59,6 +61,11 @@ namespace MinecraftClient.Crypto
|
|||
BaseStream.Flush();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return BaseStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
|
|
@ -101,6 +108,15 @@ namespace MinecraftClient.Crypto
|
|||
return (byte)(blockOutput[0] ^ inputBuf);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EncryptBlock(ReadOnlySpan<byte> blockInput, Span<byte> blockOutput)
|
||||
{
|
||||
if (FastAes is not null)
|
||||
FastAes.EncryptEcb(blockInput, blockOutput);
|
||||
else
|
||||
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public override int Read(byte[] buffer, int outOffset, int required)
|
||||
{
|
||||
|
|
@ -122,25 +138,13 @@ namespace MinecraftClient.Crypto
|
|||
}
|
||||
|
||||
int processEnd = readed + curRead;
|
||||
if (FastAes is not null)
|
||||
{
|
||||
for (int idx = readed; idx < processEnd; idx++)
|
||||
{
|
||||
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
|
||||
FastAes.EncryptEcb(blockInput, blockOutput);
|
||||
EncryptBlock(blockInput, blockOutput);
|
||||
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int idx = readed; idx < processEnd; idx++)
|
||||
{
|
||||
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
|
||||
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
|
||||
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize);
|
||||
|
||||
|
|
@ -161,10 +165,7 @@ namespace MinecraftClient.Crypto
|
|||
{
|
||||
Span<byte> blockOutput = stackalloc byte[blockSize];
|
||||
|
||||
if (FastAes is not null)
|
||||
FastAes.EncryptEcb(WriteStreamIV, blockOutput);
|
||||
else
|
||||
Aes!.EncryptEcb(WriteStreamIV, blockOutput, PaddingMode.None);
|
||||
EncryptBlock(WriteStreamIV, blockOutput);
|
||||
|
||||
byte outputBuf = (byte)(blockOutput[0] ^ b);
|
||||
|
||||
|
|
@ -185,15 +186,88 @@ namespace MinecraftClient.Crypto
|
|||
for (int wirtten = 0; wirtten < required; ++wirtten)
|
||||
{
|
||||
ReadOnlySpan<byte> blockInput = new(outputBuf, wirtten, blockSize);
|
||||
if (FastAes is not null)
|
||||
FastAes.EncryptEcb(blockInput, blockOutput);
|
||||
else
|
||||
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
|
||||
EncryptBlock(blockInput, blockOutput);
|
||||
outputBuf[blockSize + wirtten] = (byte)(blockOutput[0] ^ input[offset + wirtten]);
|
||||
}
|
||||
BaseStream.WriteAsync(outputBuf, blockSize, required);
|
||||
BaseStream.Write(outputBuf, blockSize, required);
|
||||
|
||||
Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (inStreamEnded || buffer.Length == 0)
|
||||
return 0;
|
||||
|
||||
byte[] inputBuf = new byte[blockSize + buffer.Length];
|
||||
Array.Copy(ReadStreamIV, inputBuf, blockSize);
|
||||
|
||||
for (int readed = 0; readed < buffer.Length;)
|
||||
{
|
||||
int curRead = await BaseStream.ReadAsync(inputBuf.AsMemory(blockSize + readed, buffer.Length - readed), cancellationToken);
|
||||
if (curRead == 0)
|
||||
{
|
||||
inStreamEnded = true;
|
||||
Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize);
|
||||
return readed;
|
||||
}
|
||||
|
||||
int processEnd = readed + curRead;
|
||||
DecryptToOutputBuffer(inputBuf, buffer, readed, processEnd);
|
||||
readed = processEnd;
|
||||
}
|
||||
|
||||
Array.Copy(inputBuf, buffer.Length, ReadStreamIV, 0, blockSize);
|
||||
return buffer.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (buffer.Length == 0)
|
||||
return;
|
||||
|
||||
byte[] outputBuf = new byte[blockSize + buffer.Length];
|
||||
Array.Copy(WriteStreamIV, outputBuf, blockSize);
|
||||
EncryptToOutputBuffer(buffer, outputBuf);
|
||||
|
||||
await BaseStream.WriteAsync(outputBuf.AsMemory(blockSize, buffer.Length), cancellationToken);
|
||||
Array.Copy(outputBuf, buffer.Length, WriteStreamIV, 0, blockSize);
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
private void DecryptToOutputBuffer(byte[] inputBuf, Memory<byte> output, int start, int end)
|
||||
{
|
||||
Span<byte> blockOutput = stackalloc byte[blockSize];
|
||||
for (int idx = start; idx < end; idx++)
|
||||
{
|
||||
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
|
||||
EncryptBlock(blockInput, blockOutput);
|
||||
output.Span[idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
private void EncryptToOutputBuffer(ReadOnlyMemory<byte> input, byte[] outputBuf)
|
||||
{
|
||||
Span<byte> blockOutput = stackalloc byte[blockSize];
|
||||
for (int written = 0; written < input.Length; ++written)
|
||||
{
|
||||
ReadOnlySpan<byte> blockInput = new(outputBuf, written, blockSize);
|
||||
EncryptBlock(blockInput, blockOutput);
|
||||
outputBuf[blockSize + written] = (byte)(blockOutput[0] ^ input.Span[written]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
MinecraftClient/MainThreadExecutionScope.cs
Normal file
45
MinecraftClient/MainThreadExecutionScope.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal static class MainThreadExecutionScope
|
||||
{
|
||||
private sealed class ScopeNode(object owner, ScopeNode? parent) : IDisposable
|
||||
{
|
||||
public object Owner { get; } = owner;
|
||||
public ScopeNode? Parent { get; } = parent;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!ReferenceEquals(s_currentScope.Value, this))
|
||||
throw new InvalidOperationException("Main-thread execution scope disposed out of order.");
|
||||
|
||||
s_currentScope.Value = Parent;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly AsyncLocal<ScopeNode?> s_currentScope = new();
|
||||
|
||||
public static IDisposable Enter(object owner)
|
||||
{
|
||||
ScopeNode scopeNode = new(owner, s_currentScope.Value);
|
||||
s_currentScope.Value = scopeNode;
|
||||
return scopeNode;
|
||||
}
|
||||
|
||||
public static bool IsActive(object owner)
|
||||
{
|
||||
ScopeNode? scopeNode = s_currentScope.Value;
|
||||
while (scopeNode is not null)
|
||||
{
|
||||
if (ReferenceEquals(scopeNode.Owner, owner))
|
||||
return true;
|
||||
|
||||
scopeNode = scopeNode.Parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient.Mapping
|
||||
{
|
||||
|
|
@ -150,17 +149,8 @@ namespace MinecraftClient.Mapping
|
|||
public static Queue<Location>? CalculatePath(World world, Location start, Location goal, bool allowUnsafe,
|
||||
int maxOffset, int minOffset, TimeSpan timeout)
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
Task<Queue<Location>?> pathfindingTask = Task.Factory.StartNew(() =>
|
||||
CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token));
|
||||
pathfindingTask.Wait(timeout);
|
||||
if (!pathfindingTask.IsCompleted)
|
||||
{
|
||||
cts.Cancel();
|
||||
pathfindingTask.Wait();
|
||||
}
|
||||
|
||||
return pathfindingTask.Result;
|
||||
using CancellationTokenSource cts = new(timeout);
|
||||
return CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using System.Linq;
|
|||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Exceptions;
|
||||
using MinecraftClient.ChatBots;
|
||||
|
|
@ -42,10 +44,12 @@ namespace MinecraftClient
|
|||
private readonly Queue<string> chatQueue = new();
|
||||
private static DateTime nextMessageSendTime = DateTime.MinValue;
|
||||
|
||||
private readonly Queue<Action> threadTasks = new();
|
||||
private Queue<IMainThreadTask> threadTasks = new();
|
||||
private readonly Lock threadTasksLock = new();
|
||||
private readonly Lock recipeBookLock = new();
|
||||
private readonly Lock achievementsLock = new();
|
||||
private readonly Lock consoleCommandProcessingLock = new();
|
||||
private readonly Lock networkAutoCompleteLock = new();
|
||||
|
||||
private readonly List<ChatBot> bots = new();
|
||||
private static readonly List<ChatBot> botsOnHold = new();
|
||||
|
|
@ -223,7 +227,11 @@ namespace MinecraftClient
|
|||
IMinecraftCom handler = null!;
|
||||
SessionToken _sessionToken;
|
||||
CancellationTokenSource? cmdprompt = null;
|
||||
Tuple<Thread, CancellationTokenSource>? timeoutdetector = null;
|
||||
private Channel<string>? consoleCommandChannel;
|
||||
private Task? consoleCommandProcessingTask;
|
||||
private TaskCompletionSource<string[]>? pendingNetworkAutoCompleteRequest;
|
||||
private TaskCompletionSource<bool>? pendingCommandListInitialization;
|
||||
Tuple<Task, CancellationTokenSource>? timeoutdetector = null;
|
||||
private int transferInProgress = 0;
|
||||
|
||||
public ILogger Log;
|
||||
|
|
@ -310,9 +318,10 @@ namespace MinecraftClient
|
|||
handler = Protocol.ProtocolHandler.GetProtocolHandler(client, protocolversion, forgeInfo, this);
|
||||
Log.Info(Translations.mcc_version_supported);
|
||||
|
||||
timeoutdetector = new(new Thread(new ParameterizedThreadStart(TimeoutDetector)), new CancellationTokenSource());
|
||||
timeoutdetector.Item1.Name = "MCC Connection timeout detector";
|
||||
timeoutdetector.Item1.Start(timeoutdetector.Item2.Token);
|
||||
CancellationTokenSource timeoutDetectorCancellationTokenSource = new();
|
||||
Task timeoutDetectorTask = TimeoutDetectorAsync(timeoutDetectorCancellationTokenSource.Token);
|
||||
timeoutdetector = new(timeoutDetectorTask, timeoutDetectorCancellationTokenSource);
|
||||
_ = ObserveTimeoutDetectorAsync(timeoutDetectorTask, timeoutDetectorCancellationTokenSource.Token);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -324,10 +333,7 @@ namespace MinecraftClient
|
|||
|
||||
Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
|
||||
cmdprompt = new CancellationTokenSource();
|
||||
ConsoleIO.Backend.BeginReadThread();
|
||||
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
|
||||
StartConsoleHandlers();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -369,9 +375,7 @@ namespace MinecraftClient
|
|||
}
|
||||
else if (InternalConfig.InteractiveMode)
|
||||
{
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
StopConsoleHandlers();
|
||||
Program.HandleFailure();
|
||||
}
|
||||
|
||||
|
|
@ -389,9 +393,7 @@ namespace MinecraftClient
|
|||
// kick messages and Ignore_Kick_Message is false, or retry limit reached)
|
||||
if (InternalConfig.InteractiveMode)
|
||||
{
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
StopConsoleHandlers();
|
||||
Program.HandleFailure();
|
||||
}
|
||||
|
||||
|
|
@ -415,6 +417,7 @@ namespace MinecraftClient
|
|||
try
|
||||
{
|
||||
Log.Info($"Initiating a transfer to: {newHost}:{newPort}");
|
||||
StopConsoleHandlers();
|
||||
|
||||
// Unload bots
|
||||
UnloadAllBots();
|
||||
|
|
@ -449,10 +452,7 @@ 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;
|
||||
StartConsoleHandlers();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -496,9 +496,7 @@ namespace MinecraftClient
|
|||
}
|
||||
else if (InternalConfig.InteractiveMode)
|
||||
{
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
StopConsoleHandlers();
|
||||
Program.HandleFailure();
|
||||
}
|
||||
|
||||
|
|
@ -703,15 +701,22 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
Queue<IMainThreadTask>? pendingThreadTasks = null;
|
||||
lock (threadTasksLock)
|
||||
{
|
||||
while (threadTasks.Count > 0)
|
||||
if (threadTasks.Count > 0)
|
||||
{
|
||||
Action taskToRun = threadTasks.Dequeue();
|
||||
taskToRun();
|
||||
pendingThreadTasks = threadTasks;
|
||||
threadTasks = new();
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingThreadTasks is not null)
|
||||
{
|
||||
while (pendingThreadTasks.Count > 0)
|
||||
pendingThreadTasks.Dequeue().ExecuteSynchronously();
|
||||
}
|
||||
|
||||
lock (DigLock)
|
||||
{
|
||||
if (RemainingDiggingTime > 0)
|
||||
|
|
@ -734,29 +739,44 @@ namespace MinecraftClient
|
|||
/// <summary>
|
||||
/// Periodically checks for server keepalives and consider that connection has been lost if the last received keepalive is too old.
|
||||
/// </summary>
|
||||
private void TimeoutDetector(object? o)
|
||||
private async Task TimeoutDetectorAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
UpdateKeepAlive();
|
||||
do
|
||||
using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(15));
|
||||
try
|
||||
{
|
||||
while (await periodicTimer.WaitForNextTickAsync(cancellationToken))
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(15));
|
||||
|
||||
if (((CancellationToken)o!).IsCancellationRequested)
|
||||
return;
|
||||
|
||||
lock (lastKeepAliveLock)
|
||||
{
|
||||
if (lastKeepAlive.AddSeconds(Config.Main.Advanced.TcpTimeout) < DateTime.Now)
|
||||
{
|
||||
if (((CancellationToken)o!).IsCancellationRequested)
|
||||
return;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.error_timeout);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!((CancellationToken)o!).IsCancellationRequested);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ObserveTimeoutDetectorAsync(Task timeoutDetectorTask, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await timeoutDetectorTask;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Warn(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -770,6 +790,259 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
private void StartConsoleHandlers()
|
||||
{
|
||||
if (ConsoleIO.Backend is null)
|
||||
return;
|
||||
|
||||
cmdprompt = new CancellationTokenSource();
|
||||
StartConsoleCommandProcessing(cmdprompt.Token);
|
||||
ConsoleIO.Backend.BeginReadThread();
|
||||
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
|
||||
}
|
||||
|
||||
private void StopConsoleHandlers()
|
||||
{
|
||||
if (ConsoleIO.Backend is not null)
|
||||
{
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
}
|
||||
|
||||
StopConsoleCommandProcessing();
|
||||
}
|
||||
|
||||
private void StartConsoleCommandProcessing(CancellationToken cancellationToken)
|
||||
{
|
||||
lock (consoleCommandProcessingLock)
|
||||
{
|
||||
consoleCommandChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions()
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
AllowSynchronousContinuations = false
|
||||
});
|
||||
consoleCommandProcessingTask = ProcessConsoleMessagesAsync(consoleCommandChannel.Reader, cancellationToken);
|
||||
_ = ObserveConsoleCommandProcessingAsync(consoleCommandProcessingTask, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopConsoleCommandProcessing()
|
||||
{
|
||||
Channel<string>? activeChannel;
|
||||
|
||||
lock (consoleCommandProcessingLock)
|
||||
{
|
||||
activeChannel = consoleCommandChannel;
|
||||
consoleCommandChannel = null;
|
||||
}
|
||||
|
||||
activeChannel?.Writer.TryComplete();
|
||||
|
||||
if (cmdprompt is not null)
|
||||
{
|
||||
cmdprompt.Cancel();
|
||||
cmdprompt = null;
|
||||
}
|
||||
|
||||
CancelPendingNetworkAutoComplete();
|
||||
CancelPendingCommandListInitialization();
|
||||
}
|
||||
|
||||
private async Task ObserveConsoleCommandProcessingAsync(Task processingTask, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await processingTask;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Warn(e.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (consoleCommandProcessingLock)
|
||||
{
|
||||
if (ReferenceEquals(consoleCommandProcessingTask, processingTask))
|
||||
consoleCommandProcessingTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessConsoleMessagesAsync(ChannelReader<string> channelReader, CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (string message in channelReader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (TryParseBasicIoAutocompleteRequest(message, out _))
|
||||
await HandleBasicIoAutocompleteRequestAsync(message, cancellationToken);
|
||||
else
|
||||
await InvokeOnMainThreadAsync(() => HandleCommandPromptText(message));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleBasicIoAutocompleteRequestAsync(string text, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
string[] command = text[1..].Split((char)0x00);
|
||||
if (command.Length < 2 || !command[0].Equals("autocomplete", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
await WaitForCommandListInitializationAsync(cancellationToken);
|
||||
|
||||
Task<string[]> requestTask = InvokeRequired
|
||||
? await InvokeOnMainThreadAsync(() => BeginNetworkAutoCompleteRequest(command[1]))
|
||||
: BeginNetworkAutoCompleteRequest(command[1]);
|
||||
|
||||
await requestTask.WaitAsync(cancellationToken);
|
||||
|
||||
if (command.Length > 1)
|
||||
ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult);
|
||||
else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseBasicIoAutocompleteRequest(string text, out string behindCursor)
|
||||
{
|
||||
behindCursor = string.Empty;
|
||||
|
||||
if (!ConsoleIO.BasicIO || string.IsNullOrEmpty(text) || text[0] != (char)0x00)
|
||||
return false;
|
||||
|
||||
string[] command = text[1..].Split((char)0x00);
|
||||
if (command.Length < 2 || !command[0].Equals("autocomplete", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
behindCursor = command[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task<string[]> BeginNetworkAutoCompleteRequest(string behindCursor)
|
||||
{
|
||||
if (string.IsNullOrEmpty(behindCursor))
|
||||
return Task.FromResult(Array.Empty<string>());
|
||||
|
||||
TaskCompletionSource<string[]> request = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
pendingNetworkAutoCompleteRequest?.TrySetException(new OperationCanceledException());
|
||||
pendingNetworkAutoCompleteRequest = request;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (handler.AutoComplete(behindCursor) < 0)
|
||||
{
|
||||
CompletePendingNetworkAutoComplete(Array.Empty<string>());
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
if (ReferenceEquals(pendingNetworkAutoCompleteRequest, request))
|
||||
pendingNetworkAutoCompleteRequest = null;
|
||||
}
|
||||
request.TrySetException(e);
|
||||
}
|
||||
|
||||
return request.Task;
|
||||
}
|
||||
|
||||
private void BeginCommandListInitialization()
|
||||
{
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
pendingCommandListInitialization?.TrySetCanceled();
|
||||
pendingCommandListInitialization = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompletePendingNetworkAutoComplete(string[] result)
|
||||
{
|
||||
TaskCompletionSource<string[]>? pendingRequest;
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
pendingRequest = pendingNetworkAutoCompleteRequest;
|
||||
pendingNetworkAutoCompleteRequest = null;
|
||||
}
|
||||
|
||||
pendingRequest?.TrySetResult(result);
|
||||
}
|
||||
|
||||
private void CancelPendingNetworkAutoComplete()
|
||||
{
|
||||
TaskCompletionSource<string[]>? pendingRequest;
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
pendingRequest = pendingNetworkAutoCompleteRequest;
|
||||
pendingNetworkAutoCompleteRequest = null;
|
||||
}
|
||||
|
||||
pendingRequest?.TrySetCanceled();
|
||||
}
|
||||
|
||||
private void CompletePendingCommandListInitialization()
|
||||
{
|
||||
TaskCompletionSource<bool>? pendingInitialization;
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
pendingInitialization = pendingCommandListInitialization;
|
||||
pendingCommandListInitialization = null;
|
||||
}
|
||||
|
||||
pendingInitialization?.TrySetResult(true);
|
||||
}
|
||||
|
||||
private void CancelPendingCommandListInitialization()
|
||||
{
|
||||
TaskCompletionSource<bool>? pendingInitialization;
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
pendingInitialization = pendingCommandListInitialization;
|
||||
pendingCommandListInitialization = null;
|
||||
}
|
||||
|
||||
pendingInitialization?.TrySetCanceled();
|
||||
}
|
||||
|
||||
private async Task WaitForCommandListInitializationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Task? initializationTask;
|
||||
lock (networkAutoCompleteLock)
|
||||
{
|
||||
initializationTask = pendingCommandListInitialization?.Task;
|
||||
}
|
||||
|
||||
if (initializationTask is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await initializationTask.WaitAsync(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect the client from the server (initiated from MCC)
|
||||
/// </summary>
|
||||
|
|
@ -781,6 +1054,7 @@ namespace MinecraftClient
|
|||
|
||||
botsOnHold.Clear();
|
||||
botsOnHold.AddRange(bots);
|
||||
StopConsoleHandlers();
|
||||
|
||||
if (handler is not null)
|
||||
{
|
||||
|
|
@ -788,12 +1062,6 @@ namespace MinecraftClient
|
|||
handler.Dispose();
|
||||
}
|
||||
|
||||
if (cmdprompt is not null)
|
||||
{
|
||||
cmdprompt.Cancel();
|
||||
cmdprompt = null;
|
||||
}
|
||||
|
||||
if (timeoutdetector is not null)
|
||||
{
|
||||
timeoutdetector.Item2.Cancel();
|
||||
|
|
@ -820,7 +1088,6 @@ namespace MinecraftClient
|
|||
|
||||
if (timeoutdetector is not null)
|
||||
{
|
||||
if (timeoutdetector is not null && Thread.CurrentThread != timeoutdetector.Item1)
|
||||
timeoutdetector.Item2.Cancel();
|
||||
timeoutdetector = null;
|
||||
}
|
||||
|
|
@ -872,9 +1139,7 @@ namespace MinecraftClient
|
|||
|
||||
if (!will_restart)
|
||||
{
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
StopConsoleHandlers();
|
||||
Program.HandleFailure(null, false, reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -885,16 +1150,18 @@ namespace MinecraftClient
|
|||
|
||||
private void ConsoleReaderOnMessageReceived(object? sender, string e)
|
||||
{
|
||||
Channel<string>? activeChannel;
|
||||
lock (consoleCommandProcessingLock)
|
||||
{
|
||||
activeChannel = consoleCommandChannel;
|
||||
}
|
||||
|
||||
if (client.Client is null)
|
||||
if (activeChannel is null || client.Client is null)
|
||||
return;
|
||||
|
||||
if (client.Client.Connected)
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
InvokeOnMainThread(() => HandleCommandPromptText(e));
|
||||
}).Start();
|
||||
activeChannel.Writer.TryWrite(e);
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
|
@ -916,20 +1183,10 @@ namespace MinecraftClient
|
|||
{
|
||||
if (ConsoleIO.BasicIO && text.Length > 0 && text[0] == (char)0x00)
|
||||
{
|
||||
//Process a request from the GUI
|
||||
string[] command = text[1..].Split((char)0x00);
|
||||
switch (command[0].ToLower())
|
||||
{
|
||||
case "autocomplete":
|
||||
int id = handler.AutoComplete(command[1]);
|
||||
while (!ConsoleIO.AutoCompleteDone) { Thread.Sleep(100); }
|
||||
if (command.Length > 1) { ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult); }
|
||||
else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00);
|
||||
break;
|
||||
_ = HandleBasicIoAutocompleteRequestAsync(text, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
text = text.Trim();
|
||||
|
||||
if (text.Length > 1
|
||||
|
|
@ -967,7 +1224,6 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform an internal MCC command (not a server command, use SendText() instead for that!)
|
||||
|
|
@ -1099,19 +1355,7 @@ namespace MinecraftClient
|
|||
/// <typeparam name="T">Type of the return value</typeparam>
|
||||
public T InvokeOnMainThread<T>(Func<T> task)
|
||||
{
|
||||
if (!InvokeRequired)
|
||||
{
|
||||
return task();
|
||||
}
|
||||
else
|
||||
{
|
||||
TaskWithResult<T> taskWithResult = new(task);
|
||||
lock (threadTasksLock)
|
||||
{
|
||||
threadTasks.Enqueue(taskWithResult.ExecuteSynchronously);
|
||||
}
|
||||
return taskWithResult.WaitGetResult();
|
||||
}
|
||||
return InvokeOnMainThreadAsync(task).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1126,6 +1370,37 @@ namespace MinecraftClient
|
|||
InvokeOnMainThread(() => { task(); return true; });
|
||||
}
|
||||
|
||||
private Task<T> InvokeOnMainThreadAsync<T>(Func<T> task)
|
||||
{
|
||||
if (!InvokeRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Task.FromResult(task());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException<T>(e);
|
||||
}
|
||||
}
|
||||
|
||||
TaskWithResult<T> taskWithResult = new(task);
|
||||
lock (threadTasksLock)
|
||||
{
|
||||
threadTasks.Enqueue(taskWithResult);
|
||||
}
|
||||
return taskWithResult.AsTask();
|
||||
}
|
||||
|
||||
private Task InvokeOnMainThreadAsync(Action task)
|
||||
{
|
||||
return InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
task();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all tasks
|
||||
/// </summary>
|
||||
|
|
@ -1133,7 +1408,8 @@ namespace MinecraftClient
|
|||
{
|
||||
lock (threadTasksLock)
|
||||
{
|
||||
threadTasks.Clear();
|
||||
while (threadTasks.Count > 0)
|
||||
threadTasks.Dequeue().Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1145,16 +1421,13 @@ namespace MinecraftClient
|
|||
{
|
||||
get
|
||||
{
|
||||
int callingThreadId = Environment.CurrentManagedThreadId;
|
||||
if (handler is not null)
|
||||
{
|
||||
return handler.GetNetMainThreadId() != callingThreadId;
|
||||
}
|
||||
else
|
||||
if (handler is null)
|
||||
{
|
||||
// net read thread (main thread) not yet ready
|
||||
return false;
|
||||
}
|
||||
|
||||
return !MainThreadExecutionScope.IsActive(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3040,6 +3313,7 @@ namespace MinecraftClient
|
|||
|
||||
DispatchBotEvent(bot => bot.AfterGameJoined());
|
||||
|
||||
BeginCommandListInitialization();
|
||||
ConsoleIO.InitCommandList(dispatcher);
|
||||
}
|
||||
|
||||
|
|
@ -4445,6 +4719,8 @@ namespace MinecraftClient
|
|||
public void OnAutoCompleteDone(int transactionId, string[] result)
|
||||
{
|
||||
ConsoleIO.OnAutoCompleteDone(transactionId, result);
|
||||
CompletePendingNetworkAutoComplete(result);
|
||||
CompletePendingCommandListInitialization();
|
||||
}
|
||||
|
||||
public void SetCanSendMessage(bool canSendMessage)
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@ namespace MinecraftClient
|
|||
SessionCache.Store(loginLower, session);
|
||||
|
||||
if (result == ProtocolHandler.LoginResult.Success)
|
||||
session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck(Config.Main.General.AccountType));
|
||||
session.SessionPreCheckTask = session.SessionPreCheckAsync(Config.Main.General.AccountType);
|
||||
}
|
||||
|
||||
if (result == ProtocolHandler.LoginResult.Success)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Inventory.ItemPalettes;
|
||||
using MinecraftClient.Mapping;
|
||||
|
|
@ -313,6 +315,27 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an integer from the network asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>The integer</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public async Task<int> ReadNextVarIntRAWAsync(SocketWrapper socket, CancellationToken cancellationToken)
|
||||
{
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
byte b;
|
||||
while (true)
|
||||
{
|
||||
b = (await socket.ReadDataRAWAsync(1, cancellationToken))[0];
|
||||
i |= (b & 0x7F) << j++ * 7;
|
||||
if (j > 5) throw new OverflowException("VarInt too big");
|
||||
if ((b & 0x80) != 128) break;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an integer from a cache of bytes and remove it from the cache
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Net.Sockets;
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Crypto;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Mapping;
|
||||
|
|
@ -29,7 +30,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
readonly IMinecraftComHandler handler;
|
||||
private bool encrypted = false;
|
||||
private readonly int protocolversion;
|
||||
private Tuple<Thread, CancellationTokenSource>? netRead = null;
|
||||
private Task? netReadTask;
|
||||
private CancellationTokenSource? netReadCancellationTokenSource;
|
||||
private int netReadThreadId = -1;
|
||||
Crypto.AesCfb8Stream? s;
|
||||
readonly TcpClient c;
|
||||
|
||||
|
|
@ -69,15 +72,15 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
c = Client;
|
||||
}
|
||||
|
||||
private void Updater(object? o)
|
||||
private void Updater(CancellationToken cancelToken)
|
||||
{
|
||||
var cancelToken = (CancellationToken)o!;
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
netReadThreadId = Environment.CurrentManagedThreadId;
|
||||
using IDisposable _ = MainThreadExecutionScope.Enter(handler);
|
||||
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||
long nextUpdateDue = 0;
|
||||
|
||||
|
|
@ -104,6 +107,8 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
catch (SocketException) { }
|
||||
catch (ObjectDisposedException) { }
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception) { }
|
||||
finally { netReadThreadId = -1; }
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
return;
|
||||
|
|
@ -240,9 +245,13 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
private void StartUpdating()
|
||||
{
|
||||
netRead = new(new Thread(new ParameterizedThreadStart(Updater)), new CancellationTokenSource());
|
||||
netRead.Item1.Name = "ProtocolPacketHandler";
|
||||
netRead.Item1.Start(netRead.Item2.Token);
|
||||
CancellationTokenSource netReadCts = new();
|
||||
netReadCancellationTokenSource = netReadCts;
|
||||
netReadTask = Task.Factory.StartNew(
|
||||
() => Updater(netReadCts.Token),
|
||||
netReadCts.Token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -251,7 +260,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <returns>Net read thread ID</returns>
|
||||
public int GetNetMainThreadId()
|
||||
{
|
||||
return netRead is not null ? netRead.Item1.ManagedThreadId : -1;
|
||||
return netReadThreadId;
|
||||
}
|
||||
|
||||
public bool SendCookieResponse(string name, byte[]? data)
|
||||
|
|
@ -268,9 +277,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
try
|
||||
{
|
||||
if (netRead is not null)
|
||||
if (netReadCancellationTokenSource is not null)
|
||||
{
|
||||
netRead.Item2.Cancel();
|
||||
netReadCancellationTokenSource.Cancel();
|
||||
c.Close();
|
||||
}
|
||||
}
|
||||
|
|
@ -519,6 +528,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
Receive(pid, 0, 1, SocketFlags.None);
|
||||
while (pid[0] == 0xFA) //Skip some early plugin messages
|
||||
{
|
||||
using (MainThreadExecutionScope.Enter(handler))
|
||||
ProcessPacket(pid[0]);
|
||||
Receive(pid, 0, 1, SocketFlags.None);
|
||||
}
|
||||
|
|
@ -559,8 +569,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
if (session.ServerPublicKey is not null && session.SessionPreCheckTask is not null
|
||||
&& serverIDhash == session.ServerIDhash && Enumerable.SequenceEqual(serverPublicKey, session.ServerPublicKey))
|
||||
{
|
||||
session.SessionPreCheckTask.Wait();
|
||||
if (session.SessionPreCheckTask.Result) // PreCheck Successed
|
||||
if (session.SessionPreCheckTask.IsCompletedSuccessfully && session.SessionPreCheckTask.Result)
|
||||
needCheckSession = false;
|
||||
}
|
||||
|
||||
|
|
@ -633,6 +642,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
Receive(pid, 0, 1, SocketFlags.None);
|
||||
while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages
|
||||
{
|
||||
using (MainThreadExecutionScope.Enter(handler))
|
||||
ProcessPacket(pid[0]);
|
||||
Receive(pid, 0, 1, SocketFlags.None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Security.Cryptography;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Crypto;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Inventory.ItemPalettes;
|
||||
|
|
@ -117,8 +118,11 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
readonly PacketTypePalette packetPalette;
|
||||
readonly SocketWrapper socketWrapper;
|
||||
readonly DataTypes dataTypes;
|
||||
Tuple<Thread, CancellationTokenSource>? netMain = null; // main thread
|
||||
Tuple<Thread, CancellationTokenSource>? netReader = null; // reader thread
|
||||
private Task? netMainTask;
|
||||
private CancellationTokenSource? netMainCancellationTokenSource;
|
||||
private int netMainThreadId = -1;
|
||||
private Task? netReaderTask;
|
||||
private CancellationTokenSource? netReaderCancellationTokenSource;
|
||||
readonly ILogger log;
|
||||
readonly RandomNumberGenerator randomGen;
|
||||
private bool legacyAchievementsInitialized;
|
||||
|
|
@ -278,17 +282,17 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Separate thread. Network reading loop.
|
||||
/// Serialized packet/tick loop.
|
||||
/// </summary>
|
||||
private void Updater(object? o)
|
||||
private void Updater(CancellationToken cancelToken)
|
||||
{
|
||||
var cancelToken = (CancellationToken)o!;
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
netMainThreadId = Environment.CurrentManagedThreadId;
|
||||
using IDisposable _ = MainThreadExecutionScope.Enter(handler);
|
||||
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||
long nextUpdateDue = 0;
|
||||
while (!packetQueue.IsAddingCompleted)
|
||||
|
|
@ -330,6 +334,13 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
catch (System.IO.IOException)
|
||||
{
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
netMainThreadId = -1;
|
||||
}
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
return;
|
||||
|
|
@ -340,20 +351,13 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <summary>
|
||||
/// Read and decompress packets.
|
||||
/// </summary>
|
||||
internal void PacketReader(object? o)
|
||||
internal async Task PacketReaderAsync(CancellationToken cancelToken)
|
||||
{
|
||||
var cancelToken = (CancellationToken)o!;
|
||||
while (socketWrapper.IsConnected() && !cancelToken.IsCancellationRequested)
|
||||
while (!cancelToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (socketWrapper.HasDataAvailable())
|
||||
{
|
||||
packetQueue.Add(ReadNextPacket(), cancelToken);
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
packetQueue.Add(await ReadNextPacketAsync(cancelToken), cancelToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
|
@ -375,11 +379,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
catch (Exception)
|
||||
{
|
||||
break;
|
||||
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
packetQueue.CompleteAdding();
|
||||
|
|
@ -415,6 +418,30 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return new(packetId, packetData);
|
||||
}
|
||||
|
||||
internal async Task<Tuple<int, Queue<byte>>> ReadNextPacketAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var size = await dataTypes.ReadNextVarIntRAWAsync(socketWrapper, cancellationToken); //Packet size
|
||||
Queue<byte> packetData = new(await socketWrapper.ReadDataRAWAsync(size, cancellationToken)); //Packet contents
|
||||
|
||||
if (protocolVersion >= MC_1_8_Version
|
||||
&& compression_treshold >= 0)
|
||||
{
|
||||
var sizeUncompressed = dataTypes.ReadNextVarInt(packetData);
|
||||
if (sizeUncompressed != 0)
|
||||
{
|
||||
var toDecompress = packetData.ToArray();
|
||||
var uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed);
|
||||
packetData = new Queue<byte>(uncompressed);
|
||||
}
|
||||
}
|
||||
|
||||
var packetId = dataTypes.ReadNextVarInt(packetData);
|
||||
if (handler.GetNetworkPacketCaptureEnabled())
|
||||
handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true);
|
||||
|
||||
return new(packetId, packetData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle the given packet
|
||||
/// </summary>
|
||||
|
|
@ -3844,19 +3871,17 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// </summary>
|
||||
private void StartUpdating()
|
||||
{
|
||||
Thread threadUpdater = new(new ParameterizedThreadStart(Updater))
|
||||
{
|
||||
Name = "ProtocolPacketHandler"
|
||||
};
|
||||
netMain = new Tuple<Thread, CancellationTokenSource>(threadUpdater, new CancellationTokenSource());
|
||||
threadUpdater.Start(netMain.Item2.Token);
|
||||
CancellationTokenSource netMainCts = new();
|
||||
netMainCancellationTokenSource = netMainCts;
|
||||
netMainTask = Task.Factory.StartNew(
|
||||
() => Updater(netMainCts.Token),
|
||||
netMainCts.Token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default);
|
||||
|
||||
Thread threadReader = new(new ParameterizedThreadStart(PacketReader))
|
||||
{
|
||||
Name = "ProtocolPacketReader"
|
||||
};
|
||||
netReader = new Tuple<Thread, CancellationTokenSource>(threadReader, new CancellationTokenSource());
|
||||
threadReader.Start(netReader.Item2.Token);
|
||||
CancellationTokenSource netReaderCts = new();
|
||||
netReaderCancellationTokenSource = netReaderCts;
|
||||
netReaderTask = PacketReaderAsync(netReaderCts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3865,7 +3890,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <returns>Net read thread ID</returns>
|
||||
public int GetNetMainThreadId()
|
||||
{
|
||||
return netMain is not null ? netMain.Item1.ManagedThreadId : -1;
|
||||
return netMainThreadId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3875,14 +3900,14 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
try
|
||||
{
|
||||
if (netMain is not null)
|
||||
if (netMainCancellationTokenSource is not null)
|
||||
{
|
||||
netMain.Item2.Cancel();
|
||||
netMainCancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
if (netReader is not null)
|
||||
if (netReaderCancellationTokenSource is not null)
|
||||
{
|
||||
netReader.Item2.Cancel();
|
||||
netReaderCancellationTokenSource.Cancel();
|
||||
socketWrapper.Disconnect();
|
||||
}
|
||||
}
|
||||
|
|
@ -4106,6 +4131,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return true; //No need to check session or start encryption
|
||||
}
|
||||
default:
|
||||
using (MainThreadExecutionScope.Enter(handler))
|
||||
HandlePacket(packetId, packetData);
|
||||
break;
|
||||
}
|
||||
|
|
@ -4133,8 +4159,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
&& serverIDhash == session.ServerIDhash &&
|
||||
serverPublicKey.SequenceEqual(session.ServerPublicKey))
|
||||
{
|
||||
session.SessionPreCheckTask.Wait();
|
||||
if (session.SessionPreCheckTask.Result) // PreCheck Success
|
||||
if (session.SessionPreCheckTask.IsCompletedSuccessfully && session.SessionPreCheckTask.Result)
|
||||
needCheckSession = false;
|
||||
}
|
||||
|
||||
|
|
@ -4256,6 +4281,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return true;
|
||||
}
|
||||
default:
|
||||
using (MainThreadExecutionScope.Enter(handler))
|
||||
HandlePacket(packetId, packetData);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Crypto;
|
||||
|
||||
namespace MinecraftClient.Protocol.Handlers
|
||||
|
|
@ -68,6 +71,22 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
int read = 0;
|
||||
while (read < buffer.Length)
|
||||
{
|
||||
int currentRead = encrypted
|
||||
? await s!.ReadAsync(buffer[read..], cancellationToken)
|
||||
: await c.GetStream().ReadAsync(buffer[read..], cancellationToken);
|
||||
|
||||
if (currentRead == 0)
|
||||
throw new IOException("Connection closed.");
|
||||
|
||||
read += currentRead;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read some data from the server.
|
||||
/// </summary>
|
||||
|
|
@ -84,6 +103,18 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public async Task<byte[]> ReadDataRAWAsync(int length, CancellationToken cancellationToken)
|
||||
{
|
||||
if (length > 0)
|
||||
{
|
||||
byte[] cache = new byte[length];
|
||||
await ReceiveAsync(cache, cancellationToken);
|
||||
return cache;
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send raw data to the server.
|
||||
/// </summary>
|
||||
|
|
@ -99,6 +130,17 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
c.Client.Send(buffer);
|
||||
}
|
||||
|
||||
public async Task SendDataRAWAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsConnected())
|
||||
throw new SocketException((int)SocketError.NotConnected);
|
||||
|
||||
if (encrypted)
|
||||
await s!.WriteAsync(buffer, cancellationToken);
|
||||
else
|
||||
await c.GetStream().WriteAsync(buffer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect from the server
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Net.Http.Json;
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static MinecraftClient.Settings;
|
||||
|
||||
|
|
@ -231,6 +232,8 @@ namespace MinecraftClient.Protocol.Message
|
|||
/// Specify whether translation rules have been loaded
|
||||
/// </summary>
|
||||
private static bool RulesInitialized = false;
|
||||
private static readonly Lock RulesInitializationLock = new();
|
||||
private static Task? RulesRefreshTask = null;
|
||||
|
||||
/// <summary>
|
||||
/// Set of translation rules for formatting text
|
||||
|
|
@ -243,23 +246,25 @@ namespace MinecraftClient.Protocol.Message
|
|||
/// </summary>
|
||||
public static void InitTranslations()
|
||||
{
|
||||
if (!RulesInitialized)
|
||||
lock (RulesInitializationLock)
|
||||
{
|
||||
InitRules();
|
||||
if (RulesInitialized)
|
||||
return;
|
||||
|
||||
RulesInitialized = true;
|
||||
RulesRefreshTask = InitRulesAsync();
|
||||
_ = ObserveInitRulesAsync(RulesRefreshTask);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal rule initialization method. Looks for local rule file or download it from Mojang asset servers.
|
||||
/// Internal rule initialization method. Looks for local rule file and refreshes it from Mojang asset servers if needed.
|
||||
/// </summary>
|
||||
private static void InitRules()
|
||||
private static async Task InitRulesAsync()
|
||||
{
|
||||
if (Config.Main.Advanced.Language == "en_us")
|
||||
{
|
||||
TranslationRules =
|
||||
JsonSerializer.Deserialize<Dictionary<string, string>>(
|
||||
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
|
||||
TranslationRules = LoadEmbeddedTranslationRules();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -269,21 +274,9 @@ namespace MinecraftClient.Protocol.Message
|
|||
|
||||
string languageFilePath = "lang" + Path.DirectorySeparatorChar + Config.Main.Advanced.Language + ".json";
|
||||
|
||||
// Load the external dictionary of translation rules or display an error message
|
||||
if (File.Exists(languageFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
TranslationRules =
|
||||
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
}
|
||||
if (TryLoadTranslationRulesFromFile(languageFilePath, out Dictionary<string, string>? translationRules))
|
||||
TranslationRules = translationRules;
|
||||
else TranslationRules = LoadEmbeddedTranslationRules();
|
||||
|
||||
if (TranslationRules.TryGetValue("Version", out string? version) &&
|
||||
version == Settings.TranslationsFile_Version)
|
||||
|
|
@ -296,14 +289,12 @@ namespace MinecraftClient.Protocol.Message
|
|||
// Try downloading language file from Mojang's servers?
|
||||
ConsoleIO.WriteLineFormatted(
|
||||
"§8" + string.Format(Translations.chat_download, Config.Main.Advanced.Language));
|
||||
HttpClient httpClient = new();
|
||||
using HttpClient httpClient = new();
|
||||
try
|
||||
{
|
||||
Task<string> fetch_index = httpClient.GetStringAsync(TranslationsFile_Website_Index);
|
||||
fetch_index.Wait();
|
||||
Match match = Regex.Match(fetch_index.Result,
|
||||
string fetchIndex = await httpClient.GetStringAsync(TranslationsFile_Website_Index);
|
||||
Match match = Regex.Match(fetchIndex,
|
||||
$"minecraft/lang/{Config.Main.Advanced.Language}.json" + @""":\s\{""hash"":\s""([\d\w]{40})""");
|
||||
fetch_index.Dispose();
|
||||
if (match.Success && match.Groups.Count == 2)
|
||||
{
|
||||
string hash = match.Groups[1].Value;
|
||||
|
|
@ -312,22 +303,19 @@ namespace MinecraftClient.Protocol.Message
|
|||
ConsoleIO.WriteLineFormatted(
|
||||
string.Format(Translations.chat_request, translation_file_location));
|
||||
|
||||
Task<Dictionary<string, string>?> fetckFileTask =
|
||||
httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
|
||||
fetckFileTask.Wait();
|
||||
if (fetckFileTask.Result is not null && fetckFileTask.Result.Count > 0)
|
||||
Dictionary<string, string>? fetchedFile =
|
||||
await httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
|
||||
if (fetchedFile is not null && fetchedFile.Count > 0)
|
||||
{
|
||||
TranslationRules = fetckFileTask.Result;
|
||||
TranslationRules = fetchedFile;
|
||||
TranslationRules["Version"] = TranslationsFile_Version;
|
||||
File.WriteAllText(languageFilePath,
|
||||
await File.WriteAllTextAsync(languageFilePath,
|
||||
JsonSerializer.Serialize(TranslationRules, typeof(Dictionary<string, string>)),
|
||||
Encoding.UTF8);
|
||||
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.chat_done, languageFilePath));
|
||||
return;
|
||||
}
|
||||
|
||||
fetckFileTask.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -350,15 +338,50 @@ namespace MinecraftClient.Protocol.Message
|
|||
if (Config.Logging.DebugMessages && !string.IsNullOrEmpty(e.StackTrace))
|
||||
ConsoleIO.WriteLine(e.StackTrace);
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.Dispose();
|
||||
TranslationRules = LoadEmbeddedTranslationRules();
|
||||
ConsoleIO.WriteLine(Translations.chat_use_default);
|
||||
}
|
||||
|
||||
TranslationRules =
|
||||
JsonSerializer.Deserialize<Dictionary<string, string>>(
|
||||
private static async Task ObserveInitRulesAsync(Task initRulesTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
await initRulesTask;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TranslationRules = LoadEmbeddedTranslationRules();
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> LoadEmbeddedTranslationRules()
|
||||
{
|
||||
return JsonSerializer.Deserialize<Dictionary<string, string>>(
|
||||
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
|
||||
ConsoleIO.WriteLine(Translations.chat_use_default);
|
||||
}
|
||||
|
||||
private static bool TryLoadTranslationRulesFromFile(string languageFilePath, out Dictionary<string, string>? translationRules)
|
||||
{
|
||||
translationRules = null;
|
||||
if (!File.Exists(languageFilePath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
translationRules =
|
||||
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
|
||||
return translationRules is not null;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? TranslateString(string rulename)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
{
|
||||
|
|
@ -37,7 +38,13 @@ namespace MinecraftClient.Protocol
|
|||
{
|
||||
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
|
||||
postData = string.Format(postData, clientId, code);
|
||||
return RequestToken(postData);
|
||||
return RequestTokenAsync(postData).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static Task<LoginResponse> RequestAccessTokenAsync(string code)
|
||||
{
|
||||
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
|
||||
return RequestTokenAsync(string.Format(postData, clientId, code));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -49,7 +56,13 @@ namespace MinecraftClient.Protocol
|
|||
{
|
||||
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
|
||||
postData = string.Format(postData, clientId, refreshToken);
|
||||
return RequestToken(postData);
|
||||
return RequestTokenAsync(postData).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static Task<LoginResponse> RefreshAccessTokenAsync(string refreshToken)
|
||||
{
|
||||
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
|
||||
return RequestTokenAsync(string.Format(postData, clientId, refreshToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -58,6 +71,11 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <returns>Device code response for user to complete authentication</returns>
|
||||
public static DeviceCodeResponse RequestDeviceCode()
|
||||
{
|
||||
return RequestDeviceCodeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<DeviceCodeResponse> RequestDeviceCodeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
string postData = string.Format("client_id={0}&scope=XboxLive.signin%20offline_access%20openid%20email", clientId);
|
||||
|
||||
|
|
@ -65,7 +83,7 @@ namespace MinecraftClient.Protocol
|
|||
{
|
||||
UserAgent = "MCC/" + Program.Version
|
||||
};
|
||||
var response = request.Post("application/x-www-form-urlencoded", postData);
|
||||
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
|
||||
var jsonData = Json.ParseJson(response.Body);
|
||||
|
||||
if (jsonData?["error"] is not null)
|
||||
|
|
@ -93,6 +111,11 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="interval">Polling interval in seconds</param>
|
||||
/// <returns>Login response with access token and refresh token</returns>
|
||||
public static LoginResponse PollDeviceCodeToken(string deviceCode, int expiresIn, int interval)
|
||||
{
|
||||
return PollDeviceCodeTokenAsync(deviceCode, expiresIn, interval).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<LoginResponse> PollDeviceCodeTokenAsync(string deviceCode, int expiresIn, int interval, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Per OAuth 2.0 device code spec, server may respond with "slow_down" requiring
|
||||
// the client to increase its polling interval by this amount
|
||||
|
|
@ -107,13 +130,13 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
while (stopwatch.Elapsed.TotalSeconds < expiresIn)
|
||||
{
|
||||
Thread.Sleep(pollInterval * 1000);
|
||||
await Task.Delay(TimeSpan.FromSeconds(pollInterval), cancellationToken);
|
||||
|
||||
var request = new ProxiedWebRequest(tokenUrl)
|
||||
{
|
||||
UserAgent = "MCC/" + Program.Version
|
||||
};
|
||||
var response = request.Post("application/x-www-form-urlencoded", postData);
|
||||
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
|
||||
var jsonData = Json.ParseJson(response.Body);
|
||||
|
||||
if (jsonData?["error"] is not null)
|
||||
|
|
@ -173,12 +196,17 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="postData">Complete POST data for the request</param>
|
||||
/// <returns></returns>
|
||||
private static LoginResponse RequestToken(string postData)
|
||||
{
|
||||
return RequestTokenAsync(postData).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static async Task<LoginResponse> RequestTokenAsync(string postData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ProxiedWebRequest(tokenUrl)
|
||||
{
|
||||
UserAgent = "MCC/" + Program.Version
|
||||
};
|
||||
var response = request.Post("application/x-www-form-urlencoded", postData);
|
||||
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
|
||||
var jsonData = Json.ParseJson(response.Body);
|
||||
|
||||
// Error handling
|
||||
|
|
@ -271,6 +299,11 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="loginResponse"></param>
|
||||
/// <returns></returns>
|
||||
public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse)
|
||||
{
|
||||
return XblAuthenticateAsync(loginResponse).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<XblAuthenticateResponse> XblAuthenticateAsync(Microsoft.LoginResponse loginResponse, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ProxiedWebRequest(xbl)
|
||||
{
|
||||
|
|
@ -291,7 +324,7 @@ namespace MinecraftClient.Protocol
|
|||
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
|
||||
+ "\"TokenType\": \"JWT\""
|
||||
+ "}";
|
||||
var response = request.Post("application/json", payload);
|
||||
var response = await request.PostAsync("application/json", payload, cancellationToken);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
|
|
@ -321,6 +354,11 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="xblResponse"></param>
|
||||
/// <returns></returns>
|
||||
public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
|
||||
{
|
||||
return XSTSAuthenticateAsync(xblResponse).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<XSTSAuthenticateResponse> XSTSAuthenticateAsync(XblAuthenticateResponse xblResponse, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ProxiedWebRequest(xsts)
|
||||
{
|
||||
|
|
@ -339,7 +377,7 @@ namespace MinecraftClient.Protocol
|
|||
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
|
||||
+ "\"TokenType\": \"JWT\""
|
||||
+ "}";
|
||||
var response = request.Post("application/json", payload);
|
||||
var response = await request.PostAsync("application/json", payload, cancellationToken);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
|
|
@ -404,6 +442,11 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="xstsToken"></param>
|
||||
/// <returns></returns>
|
||||
public static string LoginWithXbox(string userHash, string xstsToken)
|
||||
{
|
||||
return LoginWithXboxAsync(userHash, xstsToken).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<string> LoginWithXboxAsync(string userHash, string xstsToken, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ProxiedWebRequest(loginWithXbox)
|
||||
{
|
||||
|
|
@ -411,7 +454,7 @@ namespace MinecraftClient.Protocol
|
|||
};
|
||||
|
||||
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
|
||||
var response = request.Post("application/json", payload);
|
||||
var response = await request.PostAsync("application/json", payload, cancellationToken);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
|
|
@ -430,10 +473,15 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="accessToken"></param>
|
||||
/// <returns>True if the user own the game</returns>
|
||||
public static bool UserHasGame(string accessToken)
|
||||
{
|
||||
return UserHasGameAsync(accessToken).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<bool> UserHasGameAsync(string accessToken, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ProxiedWebRequest(ownership);
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
var response = request.Get();
|
||||
var response = await request.GetAsync(cancellationToken);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
|
|
@ -446,10 +494,15 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
|
||||
public static UserProfile GetUserProfile(string accessToken)
|
||||
{
|
||||
return GetUserProfileAsync(accessToken).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<UserProfile> GetUserProfileAsync(string accessToken, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new ProxiedWebRequest(profile);
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
var response = request.Get();
|
||||
var response = await request.GetAsync(cancellationToken);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ using System.Net.Http;
|
|||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DnsClient;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
using MinecraftClient.Protocol.Handlers.Forge;
|
||||
|
|
@ -1108,6 +1110,43 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> SessionCheckAsync(string uuid, string accesstoken, string serverhash, LoginType type)
|
||||
{
|
||||
try
|
||||
{
|
||||
string jsonRequest = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid +
|
||||
"\",\"serverId\":\"" + serverhash + "\"}";
|
||||
string host = type == LoginType.yggdrasil
|
||||
? Config.Main.General.AuthServer.Host
|
||||
: "sessionserver.mojang.com";
|
||||
int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443;
|
||||
string endpoint = type == LoginType.yggdrasil
|
||||
? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join"
|
||||
: "/session/minecraft/join";
|
||||
|
||||
bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true;
|
||||
var response = await DoHTTPSRequestAsync(
|
||||
HttpMethod.Post,
|
||||
host,
|
||||
port,
|
||||
endpoint,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "Accept", "application/json" },
|
||||
{ "Content-Type", "application/json" }
|
||||
},
|
||||
jsonRequest,
|
||||
useHttps,
|
||||
CancellationToken.None);
|
||||
|
||||
return response.StatusCode >= 200 && response.StatusCode < 300;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve available Realms worlds of a player and display them
|
||||
/// </summary>
|
||||
|
|
@ -1349,6 +1388,57 @@ namespace MinecraftClient.Protocol
|
|||
return statusCode;
|
||||
}
|
||||
|
||||
private static async Task<(int StatusCode, string Result)> DoHTTPSRequestAsync(HttpMethod method, string host, int port, string path, Dictionary<string, string> headers, string? body, bool useHttps, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host));
|
||||
|
||||
using SocketsHttpHandler handler = new();
|
||||
handler.ConnectCallback = async (ctx, ct) =>
|
||||
{
|
||||
TcpClient client = ProxyHandler.NewTcpClient(host, port, true);
|
||||
return client.GetStream();
|
||||
};
|
||||
|
||||
using HttpClient client = new(handler);
|
||||
|
||||
string scheme = useHttps ? "https" : "http";
|
||||
using HttpRequestMessage request = new(method, scheme + "://" + host + ":" + port + path);
|
||||
|
||||
string contentType = "text/plain";
|
||||
foreach (var header in headers)
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
|
||||
contentType = header.Value;
|
||||
}
|
||||
|
||||
if (body is not null)
|
||||
request.Content = new StringContent(body, Encoding.UTF8, contentType);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§8> " + request);
|
||||
|
||||
using CancellationTokenSource timeoutCancellationTokenSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeoutCancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
|
||||
using HttpResponseMessage response = await client.SendAsync(request, timeoutCancellationTokenSource.Token);
|
||||
int statusCode = (int)response.StatusCode;
|
||||
string responseBody = statusCode == 204
|
||||
? "No Content"
|
||||
: await response.Content.ReadAsStringAsync(timeoutCancellationTokenSource.Token);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLine("");
|
||||
foreach (string line in responseBody.Split('\n'))
|
||||
ConsoleIO.WriteLineFormatted("§8< " + line);
|
||||
}
|
||||
|
||||
return (statusCode, responseBody);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode a string to a json string.
|
||||
/// Will convert special chars to \u0000 unicode escape sequences.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.Collections.Specialized;
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Proxy;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
|
|
@ -72,6 +74,12 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
public Response Get() => Send(HttpMethod.Get);
|
||||
|
||||
/// <summary>
|
||||
/// Perform GET request asynchronously. Proxy is handled automatically.
|
||||
/// </summary>
|
||||
public Task<Response> GetAsync(CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpMethod.Get, cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Perform POST request. Proxy is handled automatically.
|
||||
/// </summary>
|
||||
|
|
@ -79,6 +87,14 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="body">Request body</param>
|
||||
public Response Post(string contentType, string body) => Send(HttpMethod.Post, contentType, body);
|
||||
|
||||
/// <summary>
|
||||
/// Perform POST request asynchronously. Proxy is handled automatically.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type of request body</param>
|
||||
/// <param name="body">Request body</param>
|
||||
public Task<Response> PostAsync(string contentType, string body, CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpMethod.Post, contentType, body, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Send an HTTP request. Proxy is configured automatically from Settings.
|
||||
/// </summary>
|
||||
|
|
@ -144,6 +160,66 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send an HTTP request asynchronously. Proxy is configured automatically from Settings.
|
||||
/// </summary>
|
||||
private async Task<Response> SendAsync(HttpMethod method, string? contentType = null, string? body = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var handler = CreateHandler();
|
||||
using var client = new HttpClient(handler);
|
||||
|
||||
using var request = new HttpRequestMessage(method, _uri);
|
||||
|
||||
foreach (string key in Headers)
|
||||
{
|
||||
if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) ||
|
||||
key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
|
||||
key.Equals("Host", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
request.Headers.TryAddWithoutValidation(key, Headers[key]);
|
||||
}
|
||||
|
||||
if (body is not null)
|
||||
request.Content = new StringContent(body, Encoding.UTF8, contentType ?? "text/plain");
|
||||
|
||||
if (Debug)
|
||||
{
|
||||
ConsoleIO.WriteLine($"< {method} {_uri}");
|
||||
foreach (string key in Headers)
|
||||
ConsoleIO.WriteLine($"< {key}: {Headers[key]}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var httpResponse = await client.SendAsync(request, cancellationToken);
|
||||
string responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
var responseHeaders = new NameValueCollection();
|
||||
foreach (var header in httpResponse.Headers)
|
||||
foreach (var val in header.Value)
|
||||
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
|
||||
foreach (var header in httpResponse.Content.Headers)
|
||||
foreach (var val in header.Value)
|
||||
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
|
||||
|
||||
var cookies = new NameValueCollection();
|
||||
foreach (Cookie cookie in handler.CookieContainer.GetCookies(_uri))
|
||||
{
|
||||
if (!cookie.Expired)
|
||||
cookies.Add(cookie.Name, cookie.Value);
|
||||
}
|
||||
|
||||
return new Response((int)httpResponse.StatusCode, responseBody, responseHeaders, cookies);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (Debug)
|
||||
ConsoleIO.WriteLine("HTTP error: " + ex.Message);
|
||||
return Response.Empty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a SocketsHttpHandler with proxy support from ProxyHandler settings.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ namespace MinecraftClient.Protocol.Session
|
|||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> SessionPreCheckAsync(LoginType type)
|
||||
{
|
||||
if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey is null)
|
||||
return false;
|
||||
|
||||
Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey();
|
||||
string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey);
|
||||
return await ProtocolHandler.SessionCheckAsync(PlayerID, ID, serverHash, type);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal interface IMainThreadTask
|
||||
{
|
||||
void ExecuteSynchronously();
|
||||
void Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds an asynchronous task with return value
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the return value</typeparam>
|
||||
public class TaskWithResult<T>
|
||||
public sealed class TaskWithResult<T> : IMainThreadTask
|
||||
{
|
||||
private readonly AutoResetEvent resultEvent = new(false);
|
||||
private readonly Func<T> task;
|
||||
private T? result = default;
|
||||
private Exception? exception = null;
|
||||
private bool taskRun = false;
|
||||
private readonly Lock taskRunLock = new();
|
||||
private readonly TaskCompletionSource<T> completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private int taskState;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new asynchronous task with return value
|
||||
|
|
@ -28,13 +32,7 @@ namespace MinecraftClient
|
|||
/// <summary>
|
||||
/// Check whether the task has finished running
|
||||
/// </summary>
|
||||
public bool HasRun
|
||||
{
|
||||
get
|
||||
{
|
||||
return taskRun;
|
||||
}
|
||||
}
|
||||
public bool HasRun => completionSource.Task.IsCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Get the task result (return value of the inner delegate)
|
||||
|
|
@ -44,10 +42,10 @@ namespace MinecraftClient
|
|||
{
|
||||
get
|
||||
{
|
||||
if (taskRun)
|
||||
return result!;
|
||||
else
|
||||
if (!completionSource.Task.IsCompleted)
|
||||
throw new InvalidOperationException("Attempting to retrieve the result of an unfinished task");
|
||||
|
||||
return completionSource.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,40 +56,39 @@ namespace MinecraftClient
|
|||
{
|
||||
get
|
||||
{
|
||||
return exception;
|
||||
return completionSource.Task.Exception?.InnerException;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<T> AsTask()
|
||||
{
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute the task in the current thread and set the <see cref="Result"/> property or <see cref=""/>to the returned value
|
||||
/// </summary>
|
||||
public void ExecuteSynchronously()
|
||||
{
|
||||
// Make sur the task will not run twice
|
||||
lock (taskRunLock)
|
||||
{
|
||||
if (taskRun)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref taskState, 1, 0) != 0)
|
||||
throw new InvalidOperationException("Attempting to run a task twice");
|
||||
}
|
||||
}
|
||||
|
||||
// Run the task
|
||||
try
|
||||
{
|
||||
result = task();
|
||||
completionSource.TrySetResult(task());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
exception = e;
|
||||
completionSource.TrySetException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark task as complete and release wait event
|
||||
lock (taskRunLock)
|
||||
public void Cancel()
|
||||
{
|
||||
taskRun = true;
|
||||
}
|
||||
resultEvent.Set();
|
||||
if (Interlocked.CompareExchange(ref taskState, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
completionSource.TrySetException(new OperationCanceledException("Main-thread task was canceled before execution."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -101,22 +98,7 @@ namespace MinecraftClient
|
|||
/// <exception cref="System.Exception">Any exception thrown by the task</exception>
|
||||
public T WaitGetResult()
|
||||
{
|
||||
// Wait only if the result is not available yet
|
||||
bool mustWait = false;
|
||||
lock (taskRunLock)
|
||||
{
|
||||
mustWait = !taskRun;
|
||||
}
|
||||
if (mustWait)
|
||||
{
|
||||
resultEvent.WaitOne();
|
||||
}
|
||||
|
||||
// Receive exception from task
|
||||
if (exception is not null)
|
||||
throw exception;
|
||||
|
||||
return result!;
|
||||
return completionSource.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue