mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Dialog System First iteration, not tested
This commit is contained in:
parent
37cf19c175
commit
3d988c932f
14 changed files with 2250 additions and 20 deletions
475
MinecraftClient/Protocol/Dialogs/DialogNbtParser.cs
Normal file
475
MinecraftClient/Protocol/Dialogs/DialogNbtParser.cs
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MinecraftClient.Dialogs;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.Dialogs;
|
||||
|
||||
public sealed class DialogNbtParser
|
||||
{
|
||||
public DialogDefinition Parse(Dictionary<string, object> nbt)
|
||||
{
|
||||
var type = NormalizeType(GetString(nbt, "type") ?? "minecraft:notice");
|
||||
var common = ParseCommon(nbt, type);
|
||||
var actions = new List<DialogButton>();
|
||||
DialogActionDefinition? cancelAction = null;
|
||||
var columns = GetInt(nbt, "columns", 1);
|
||||
var buttonWidth = GetInt(nbt, "button_width", 150);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "minecraft:notice":
|
||||
var noticeAction = ParseButton(nbt, "action", 1);
|
||||
actions.Add(noticeAction ?? new DialogButton(1, Translations.dialog_action_ok, null));
|
||||
cancelAction = actions[0].Action;
|
||||
break;
|
||||
|
||||
case "minecraft:confirmation":
|
||||
AddIfNotNull(actions, ParseButton(nbt, "yes", 1));
|
||||
AddIfNotNull(actions, ParseButton(nbt, "no", 2));
|
||||
cancelAction = actions.Count >= 2 ? actions[1].Action : null;
|
||||
break;
|
||||
|
||||
case "minecraft:multi_action":
|
||||
actions.AddRange(ParseButtonList(GetValue(nbt, "actions")));
|
||||
cancelAction = ParseButton(nbt, "exit_action", 0)?.Action;
|
||||
break;
|
||||
|
||||
case "minecraft:dialog_list":
|
||||
actions.AddRange(ParseDialogListActions(GetValue(nbt, "dialogs")));
|
||||
cancelAction = ParseButton(nbt, "exit_action", 0)?.Action;
|
||||
break;
|
||||
|
||||
case "minecraft:server_links":
|
||||
cancelAction = ParseButton(nbt, "exit_action", 0)?.Action;
|
||||
break;
|
||||
}
|
||||
|
||||
return new DialogDefinition(
|
||||
type,
|
||||
common.Title,
|
||||
common.ExternalTitle,
|
||||
common.CanCloseWithEscape,
|
||||
common.Pause,
|
||||
common.AfterAction,
|
||||
common.Body,
|
||||
common.Inputs,
|
||||
actions,
|
||||
cancelAction,
|
||||
columns,
|
||||
buttonWidth);
|
||||
}
|
||||
|
||||
public DialogDefinition? TryParse(Dictionary<string, object>? nbt)
|
||||
{
|
||||
return nbt is null ? null : Parse(nbt);
|
||||
}
|
||||
|
||||
private static DialogCommon ParseCommon(Dictionary<string, object> nbt, string type)
|
||||
{
|
||||
var title = ParseComponent(GetValue(nbt, "title"));
|
||||
var externalTitle = nbt.TryGetValue("external_title", out var externalTitleValue)
|
||||
? ParseComponent(externalTitleValue)
|
||||
: null;
|
||||
var canCloseWithEscape = GetBool(nbt, "can_close_with_escape", true);
|
||||
var pause = GetBool(nbt, "pause", true);
|
||||
var afterAction = ParseAfterAction(GetString(nbt, "after_action") ?? "close");
|
||||
var body = ParseBody(GetValue(nbt, "body"));
|
||||
var inputs = ParseInputs(GetValue(nbt, "inputs"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
title = type;
|
||||
|
||||
return new DialogCommon(title, externalTitle, canCloseWithEscape, pause, afterAction, body, inputs);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DialogBody> ParseBody(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return [];
|
||||
|
||||
List<DialogBody> body = [];
|
||||
foreach (var item in Enumerate(value))
|
||||
{
|
||||
if (item is Dictionary<string, object> compound)
|
||||
{
|
||||
var type = NormalizeType(GetString(compound, "type") ?? "minecraft:plain_message");
|
||||
if (type == "minecraft:item")
|
||||
{
|
||||
var description = compound.TryGetValue("description", out var desc)
|
||||
? ParsePlainMessage(desc)
|
||||
: string.Empty;
|
||||
body.Add(new DialogBody(DialogBodyKind.Item, string.IsNullOrWhiteSpace(description) ? Translations.dialog_item_body : description, type));
|
||||
continue;
|
||||
}
|
||||
|
||||
body.Add(new DialogBody(DialogBodyKind.PlainMessage, ParsePlainMessage(compound), type));
|
||||
continue;
|
||||
}
|
||||
|
||||
body.Add(new DialogBody(DialogBodyKind.PlainMessage, ParseComponent(item), "minecraft:plain_message"));
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private static string ParsePlainMessage(object? value)
|
||||
{
|
||||
if (value is Dictionary<string, object> compound && compound.TryGetValue("contents", out var contents))
|
||||
return ParseComponent(contents);
|
||||
|
||||
return ParseComponent(value);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DialogInput> ParseInputs(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return [];
|
||||
|
||||
List<DialogInput> inputs = [];
|
||||
foreach (var item in Enumerate(value))
|
||||
{
|
||||
if (item is not Dictionary<string, object> inputData)
|
||||
continue;
|
||||
|
||||
var key = GetString(inputData, "key");
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
continue;
|
||||
|
||||
var control = inputData.TryGetValue("control", out var controlValue) && controlValue is Dictionary<string, object> controlData
|
||||
? controlData
|
||||
: inputData;
|
||||
|
||||
var type = NormalizeType(GetString(control, "type") ?? "minecraft:text");
|
||||
inputs.Add(type switch
|
||||
{
|
||||
"minecraft:boolean" => ParseBooleanInput(key, type, control),
|
||||
"minecraft:number_range" => ParseNumberInput(key, type, control),
|
||||
"minecraft:single_option" => ParseOptionInput(key, type, control),
|
||||
"minecraft:text" => ParseTextInput(key, type, control),
|
||||
_ => new DialogInput(key, DialogInputKind.Unknown, ParseComponent(GetValue(control, "label")), string.Empty, Type: type)
|
||||
});
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
private static DialogInput ParseTextInput(string key, string type, Dictionary<string, object> control)
|
||||
{
|
||||
return new DialogInput(
|
||||
key,
|
||||
DialogInputKind.Text,
|
||||
ParseComponent(GetValue(control, "label")),
|
||||
GetString(control, "initial") ?? string.Empty,
|
||||
MaxLength: GetInt(control, "max_length", 32),
|
||||
LabelVisible: GetBool(control, "label_visible", true),
|
||||
Multiline: control.ContainsKey("multiline"),
|
||||
Type: type);
|
||||
}
|
||||
|
||||
private static DialogInput ParseBooleanInput(string key, string type, Dictionary<string, object> control)
|
||||
{
|
||||
var initial = GetBool(control, "initial", false);
|
||||
return new DialogInput(
|
||||
key,
|
||||
DialogInputKind.Boolean,
|
||||
ParseComponent(GetValue(control, "label")),
|
||||
initial ? "true" : "false",
|
||||
OnTrue: GetString(control, "on_true") ?? "true",
|
||||
OnFalse: GetString(control, "on_false") ?? "false",
|
||||
Type: type);
|
||||
}
|
||||
|
||||
private static DialogInput ParseOptionInput(string key, string type, Dictionary<string, object> control)
|
||||
{
|
||||
var options = ParseOptions(GetValue(control, "options"));
|
||||
var initial = options.FirstOrDefault(static option => option.Initial)?.Id
|
||||
?? options.FirstOrDefault()?.Id
|
||||
?? string.Empty;
|
||||
return new DialogInput(
|
||||
key,
|
||||
DialogInputKind.SingleOption,
|
||||
ParseComponent(GetValue(control, "label")),
|
||||
initial,
|
||||
LabelVisible: GetBool(control, "label_visible", true),
|
||||
Options: options,
|
||||
Type: type);
|
||||
}
|
||||
|
||||
private static DialogInput ParseNumberInput(string key, string type, Dictionary<string, object> control)
|
||||
{
|
||||
var range = control.TryGetValue("range_info", out var rangeValue) && rangeValue is Dictionary<string, object> rangeData
|
||||
? rangeData
|
||||
: control;
|
||||
var start = GetFloat(range, "start", 0);
|
||||
var end = GetFloat(range, "end", 1);
|
||||
var initial = TryGetFloat(range, "initial") ?? ((start + end) / 2F);
|
||||
return new DialogInput(
|
||||
key,
|
||||
DialogInputKind.NumberRange,
|
||||
ParseComponent(GetValue(control, "label")),
|
||||
NumberToString(initial),
|
||||
Start: start,
|
||||
End: end,
|
||||
InitialNumber: initial,
|
||||
Step: TryGetFloat(range, "step"),
|
||||
Type: type);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DialogOption> ParseOptions(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return [];
|
||||
|
||||
List<DialogOption> options = [];
|
||||
foreach (var item in Enumerate(value))
|
||||
{
|
||||
if (item is string id)
|
||||
{
|
||||
options.Add(new DialogOption(id, id, false));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item is Dictionary<string, object> option)
|
||||
{
|
||||
var optionId = GetString(option, "id");
|
||||
if (optionId is null)
|
||||
continue;
|
||||
|
||||
var display = option.TryGetValue("display", out var displayValue)
|
||||
? ParseComponent(displayValue)
|
||||
: optionId;
|
||||
options.Add(new DialogOption(optionId, display, GetBool(option, "initial", false)));
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static List<DialogButton> ParseButtonList(object? value)
|
||||
{
|
||||
List<DialogButton> buttons = [];
|
||||
var index = 1;
|
||||
foreach (var item in Enumerate(value))
|
||||
{
|
||||
if (item is Dictionary<string, object> buttonData)
|
||||
buttons.Add(ParseButton(buttonData, index++) ?? new DialogButton(index - 1, Translations.dialog_action_unnamed, null));
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static IEnumerable<DialogButton> ParseDialogListActions(object? value)
|
||||
{
|
||||
List<DialogButton> buttons = [];
|
||||
var index = 1;
|
||||
foreach (var item in Enumerate(value))
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case string tag when tag.StartsWith('#'):
|
||||
buttons.Add(new DialogButton(index++, tag, new DialogActionDefinition(DialogActionKind.Unknown, Type: "dialog_tag")));
|
||||
break;
|
||||
case string resource:
|
||||
buttons.Add(new DialogButton(index++, resource, new DialogActionDefinition(DialogActionKind.ShowDialog, Value: resource, Type: "dialog_reference_name")));
|
||||
break;
|
||||
case Dictionary<string, object> dialog:
|
||||
var nested = new DialogNbtParser().Parse(dialog);
|
||||
buttons.Add(new DialogButton(index++, nested.DisplayTitle(), new DialogActionDefinition(DialogActionKind.ShowDialog, NestedDialog: nested)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static DialogButton? ParseButton(Dictionary<string, object> owner, string key, int index)
|
||||
{
|
||||
return owner.TryGetValue(key, out var value) && value is Dictionary<string, object> data
|
||||
? ParseButton(data, index)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static DialogButton? ParseButton(Dictionary<string, object> data, int index)
|
||||
{
|
||||
var label = data.TryGetValue("label", out var labelValue)
|
||||
? ParseComponent(labelValue)
|
||||
: Translations.dialog_action_unnamed;
|
||||
var action = data.TryGetValue("action", out var actionValue) && actionValue is Dictionary<string, object> actionData
|
||||
? ParseAction(actionData)
|
||||
: null;
|
||||
return new DialogButton(index, label, action);
|
||||
}
|
||||
|
||||
private static DialogActionDefinition ParseAction(Dictionary<string, object> action)
|
||||
{
|
||||
var type = NormalizeType(GetString(action, "type") ?? GetString(action, "action") ?? "minecraft:none");
|
||||
return type switch
|
||||
{
|
||||
"minecraft:run_command" => new DialogActionDefinition(DialogActionKind.RunCommand, Value: GetString(action, "command"), Type: type),
|
||||
"minecraft:dynamic/run_command" => new DialogActionDefinition(DialogActionKind.RunCommand, Value: GetString(action, "template"), Type: type),
|
||||
"minecraft:custom" => new DialogActionDefinition(DialogActionKind.CustomClick, Id: GetString(action, "id"), Payload: GetCompound(action, "payload"), Type: type),
|
||||
"minecraft:dynamic/custom" => new DialogActionDefinition(DialogActionKind.CustomClick, Id: GetString(action, "id"), Payload: GetCompound(action, "additions"), Type: type),
|
||||
"minecraft:open_url" => new DialogActionDefinition(DialogActionKind.OpenUrl, Value: GetString(action, "url"), Type: type),
|
||||
"minecraft:suggest_command" => new DialogActionDefinition(DialogActionKind.SuggestCommand, Value: GetString(action, "command"), Type: type),
|
||||
"minecraft:copy_to_clipboard" => new DialogActionDefinition(DialogActionKind.CopyToClipboard, Value: GetString(action, "value"), Type: type),
|
||||
"minecraft:show_dialog" => ParseShowDialogAction(action, type),
|
||||
_ => new DialogActionDefinition(DialogActionKind.Unknown, Type: type)
|
||||
};
|
||||
}
|
||||
|
||||
private static DialogActionDefinition ParseShowDialogAction(Dictionary<string, object> action, string type)
|
||||
{
|
||||
if (!action.TryGetValue("dialog", out var value))
|
||||
return new DialogActionDefinition(DialogActionKind.ShowDialog, Type: type);
|
||||
|
||||
if (value is Dictionary<string, object> dialogData)
|
||||
return new DialogActionDefinition(DialogActionKind.ShowDialog, NestedDialog: new DialogNbtParser().Parse(dialogData), Type: type);
|
||||
|
||||
return new DialogActionDefinition(DialogActionKind.ShowDialog, Value: value.ToString(), Type: type);
|
||||
}
|
||||
|
||||
private static DialogAfterAction ParseAfterAction(string value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
"none" => DialogAfterAction.None,
|
||||
"wait_for_response" => DialogAfterAction.WaitForResponse,
|
||||
_ => DialogAfterAction.Close
|
||||
};
|
||||
}
|
||||
|
||||
private static string ParseComponent(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
Dictionary<string, object> compound => ChatParser.ParseText(compound),
|
||||
string text when text.StartsWith('{') || text.StartsWith('[') => ChatParser.ParseText(text),
|
||||
string text => text,
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<object> Enumerate(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
yield break;
|
||||
|
||||
if (value is object[] array)
|
||||
{
|
||||
foreach (var item in array)
|
||||
yield return item;
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return value;
|
||||
}
|
||||
|
||||
private static object? GetValue(Dictionary<string, object> data, string key)
|
||||
{
|
||||
return data.TryGetValue(key, out var value) ? value : null;
|
||||
}
|
||||
|
||||
private static string? GetString(Dictionary<string, object> data, string key)
|
||||
{
|
||||
return data.TryGetValue(key, out var value) ? value as string ?? value.ToString() : null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object>? GetCompound(Dictionary<string, object> data, string key)
|
||||
{
|
||||
return data.TryGetValue(key, out var value) && value is Dictionary<string, object> compound ? compound : null;
|
||||
}
|
||||
|
||||
private static bool GetBool(Dictionary<string, object> data, string key, bool fallback)
|
||||
{
|
||||
if (!data.TryGetValue(key, out var value))
|
||||
return fallback;
|
||||
|
||||
return value switch
|
||||
{
|
||||
bool boolean => boolean,
|
||||
byte number => number != 0,
|
||||
sbyte number => number != 0,
|
||||
int number => number != 0,
|
||||
string text when bool.TryParse(text, out var parsed) => parsed,
|
||||
_ => fallback
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetInt(Dictionary<string, object> data, string key, int fallback)
|
||||
{
|
||||
if (!data.TryGetValue(key, out var value))
|
||||
return fallback;
|
||||
|
||||
return value switch
|
||||
{
|
||||
byte number => number,
|
||||
short number => number,
|
||||
int number => number,
|
||||
long number => (int)number,
|
||||
string text when int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) => parsed,
|
||||
_ => fallback
|
||||
};
|
||||
}
|
||||
|
||||
private static float GetFloat(Dictionary<string, object> data, string key, float fallback)
|
||||
{
|
||||
return TryGetFloat(data, key) ?? fallback;
|
||||
}
|
||||
|
||||
private static float? TryGetFloat(Dictionary<string, object> data, string key)
|
||||
{
|
||||
if (!data.TryGetValue(key, out var value))
|
||||
return null;
|
||||
|
||||
return value switch
|
||||
{
|
||||
float number => number,
|
||||
double number => (float)number,
|
||||
int number => number,
|
||||
long number => number,
|
||||
string text when float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) => parsed,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeType(string type)
|
||||
{
|
||||
return type.Contains(':', StringComparison.Ordinal) ? type : "minecraft:" + type;
|
||||
}
|
||||
|
||||
private static void AddIfNotNull(List<DialogButton> buttons, DialogButton? button)
|
||||
{
|
||||
if (button is not null)
|
||||
buttons.Add(button);
|
||||
}
|
||||
|
||||
private static string NumberToString(float value)
|
||||
{
|
||||
var integer = (int)value;
|
||||
return integer == value
|
||||
? integer.ToString(CultureInfo.InvariantCulture)
|
||||
: value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private sealed record DialogCommon(
|
||||
string Title,
|
||||
string? ExternalTitle,
|
||||
bool CanCloseWithEscape,
|
||||
bool Pause,
|
||||
DialogAfterAction AfterAction,
|
||||
IReadOnlyList<DialogBody> Body,
|
||||
IReadOnlyList<DialogInput> Inputs);
|
||||
}
|
||||
|
|
@ -266,6 +266,11 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool SendCustomClickAction(string id, Dictionary<string, object>? payload)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Crypto;
|
||||
using MinecraftClient.Dialogs;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Inventory.ItemPalettes;
|
||||
using MinecraftClient.Logger;
|
||||
|
|
@ -19,6 +20,7 @@ using MinecraftClient.Mapping.EntityPalettes;
|
|||
using MinecraftClient.Protocol.Handlers.Forge;
|
||||
using MinecraftClient.Protocol.Handlers.packet.s2c;
|
||||
using MinecraftClient.Protocol.Handlers.PacketPalettes;
|
||||
using MinecraftClient.Protocol.Dialogs;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
using MinecraftClient.Protocol.ProfileKey;
|
||||
using MinecraftClient.Protocol.Session;
|
||||
|
|
@ -118,6 +120,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
readonly PacketTypePalette packetPalette;
|
||||
readonly SocketWrapper socketWrapper;
|
||||
readonly DataTypes dataTypes;
|
||||
readonly DialogNbtParser dialogNbtParser = new();
|
||||
Tuple<Thread, CancellationTokenSource>? netMain = null; // main thread
|
||||
Tuple<Thread, CancellationTokenSource>? netReader = null; // reader thread
|
||||
readonly ILogger log;
|
||||
|
|
@ -564,6 +567,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
var isDimension = registryId == "minecraft:dimension_type";
|
||||
var isAttribute = registryId == "minecraft:attribute";
|
||||
var isEnchantment = registryId == "minecraft:enchantment";
|
||||
var isDialog = registryId == "minecraft:dialog";
|
||||
|
||||
var availableChats = isChat ? new Dictionary<int, string>() : null;
|
||||
var dimensionIdMap = isDimension ? new Dictionary<int, string>() : null;
|
||||
|
|
@ -596,6 +600,8 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
else if (isEnchantment)
|
||||
enchantmentIdMap!.Add(i, entryId);
|
||||
else if (isDialog && nbtData is not null)
|
||||
handler.OnDialogRegistryData(i, entryId, dialogNbtParser.Parse(nbtData));
|
||||
}
|
||||
|
||||
if (isChat)
|
||||
|
|
@ -671,16 +677,15 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
break;
|
||||
|
||||
case ConfigurationPacketTypesIn.ServerLinks:
|
||||
var cfgLinksCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (var i = 0; i < cfgLinksCount; i++)
|
||||
{
|
||||
var cfgIsBuiltIn = dataTypes.ReadNextBool(packetData);
|
||||
if (cfgIsBuiltIn)
|
||||
dataTypes.ReadNextVarInt(packetData); // Known type ID
|
||||
else
|
||||
dataTypes.ReadNextChat(packetData); // Component label
|
||||
dataTypes.ReadNextString(packetData); // URL
|
||||
}
|
||||
handler.OnServerLinksUpdated(ReadServerLinks(packetData));
|
||||
break;
|
||||
|
||||
case ConfigurationPacketTypesIn.ClearDialog:
|
||||
handler.OnDialogCleared();
|
||||
break;
|
||||
|
||||
case ConfigurationPacketTypesIn.ShowDialog:
|
||||
HandleShowDialog(packetData, DialogPhase.Configuration);
|
||||
break;
|
||||
|
||||
// Ignore other packets at this stage
|
||||
|
|
@ -3373,16 +3378,15 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
break;
|
||||
|
||||
case PacketTypesIn.ServerLinks:
|
||||
var linksCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (var i = 0; i < linksCount; i++)
|
||||
{
|
||||
var isBuiltIn = dataTypes.ReadNextBool(packetData);
|
||||
if (isBuiltIn)
|
||||
dataTypes.ReadNextVarInt(packetData); // Known type ID
|
||||
else
|
||||
dataTypes.ReadNextChat(packetData); // Component label
|
||||
dataTypes.ReadNextString(packetData); // URL
|
||||
}
|
||||
handler.OnServerLinksUpdated(ReadServerLinks(packetData));
|
||||
break;
|
||||
|
||||
case PacketTypesIn.ClearDialog:
|
||||
handler.OnDialogCleared();
|
||||
break;
|
||||
|
||||
case PacketTypesIn.ShowDialog:
|
||||
HandleShowDialog(packetData, DialogPhase.Play);
|
||||
break;
|
||||
|
||||
// 1.21.2+ new packets
|
||||
|
|
@ -4105,6 +4109,61 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
SendPacket(packetPalette.GetOutgoingIdByTypeConfiguration(packet), packetData, packet.ToString());
|
||||
}
|
||||
|
||||
private void HandleShowDialog(Queue<byte> packetData, DialogPhase phase)
|
||||
{
|
||||
if (phase == DialogPhase.Play)
|
||||
{
|
||||
var holderId = dataTypes.ReadNextVarInt(packetData);
|
||||
if (holderId != 0)
|
||||
{
|
||||
handler.OnDialogRegistryReferenceShown(holderId - 1, phase);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var dialog = dialogNbtParser.Parse(dataTypes.ReadNextNbt(packetData));
|
||||
handler.OnDialogShown(dialog, phase);
|
||||
}
|
||||
|
||||
private IReadOnlyList<DialogServerLink> ReadServerLinks(Queue<byte> packetData)
|
||||
{
|
||||
var linksCount = dataTypes.ReadNextVarInt(packetData);
|
||||
List<DialogServerLink> links = new(linksCount);
|
||||
|
||||
for (var i = 0; i < linksCount; i++)
|
||||
{
|
||||
string label;
|
||||
var isBuiltIn = dataTypes.ReadNextBool(packetData);
|
||||
if (isBuiltIn)
|
||||
label = GetKnownServerLinkLabel(dataTypes.ReadNextVarInt(packetData));
|
||||
else
|
||||
label = dataTypes.ReadNextChat(packetData);
|
||||
|
||||
var url = dataTypes.ReadNextString(packetData);
|
||||
links.Add(new DialogServerLink(label, url));
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
private static string GetKnownServerLinkLabel(int id)
|
||||
{
|
||||
return id switch
|
||||
{
|
||||
0 => Translations.dialog_server_link_report_bug,
|
||||
1 => Translations.dialog_server_link_community_guidelines,
|
||||
2 => Translations.dialog_server_link_support,
|
||||
3 => Translations.dialog_server_link_status,
|
||||
4 => Translations.dialog_server_link_feedback,
|
||||
5 => Translations.dialog_server_link_community,
|
||||
6 => Translations.dialog_server_link_website,
|
||||
7 => Translations.dialog_server_link_forums,
|
||||
8 => Translations.dialog_server_link_news,
|
||||
9 => Translations.dialog_server_link_announcements,
|
||||
_ => id.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a packet to the server. Compression and encryption will be handled automatically.
|
||||
/// </summary>
|
||||
|
|
@ -5025,6 +5084,49 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
public bool SendCustomClickAction(string id, Dictionary<string, object>? payload)
|
||||
{
|
||||
if (protocolVersion < MC_1_21_6_Version)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
List<byte> fields = new();
|
||||
fields.AddRange(dataTypes.GetString(id.Contains(':', StringComparison.Ordinal) ? id : "minecraft:" + id));
|
||||
|
||||
var tagBytes = dataTypes.GetNbtTag(payload);
|
||||
if (tagBytes.Length > 65536)
|
||||
return false;
|
||||
|
||||
fields.AddRange(DataTypes.GetVarInt(tagBytes.Length));
|
||||
fields.AddRange(tagBytes);
|
||||
|
||||
switch (currentState)
|
||||
{
|
||||
case CurrentState.Configuration:
|
||||
SendPacket(ConfigurationPacketTypesOut.CustomClickAction, fields);
|
||||
return true;
|
||||
case CurrentState.Play:
|
||||
SendPacket(PacketTypesOut.CustomClickAction, fields);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (System.IO.IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat message to the server
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ namespace MinecraftClient.Protocol
|
|||
/// <returns>True if successfully sent</returns>
|
||||
bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
|
||||
|
||||
/// <summary>
|
||||
/// Send a custom click action packet introduced for dialogs in Minecraft 1.21.6.
|
||||
/// </summary>
|
||||
/// <param name="id">Custom action resource location</param>
|
||||
/// <param name="payload">Optional NBT payload</param>
|
||||
/// <returns>True if successfully sent</returns>
|
||||
bool SendCustomClickAction(string id, Dictionary<string, object>? payload);
|
||||
|
||||
/// <summary>
|
||||
/// Allow to respawn after death
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MinecraftClient.Dialogs;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Logger;
|
||||
using MinecraftClient.Mapping;
|
||||
|
|
@ -93,6 +94,31 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="message">Message received</param>
|
||||
public void OnTextReceived(ChatMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server synchronizes a dialog registry entry.
|
||||
/// </summary>
|
||||
void OnDialogRegistryData(int protocolId, string resourceId, DialogDefinition dialog);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server shows a custom dialog.
|
||||
/// </summary>
|
||||
void OnDialogShown(DialogDefinition dialog, DialogPhase phase);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server shows a custom dialog by registry protocol ID.
|
||||
/// </summary>
|
||||
void OnDialogRegistryReferenceShown(int protocolId, DialogPhase phase);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server clears the current custom dialog.
|
||||
/// </summary>
|
||||
void OnDialogCleared();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server sends updated server links.
|
||||
/// </summary>
|
||||
void OnServerLinksUpdated(IReadOnlyList<DialogServerLink> links);
|
||||
|
||||
/// <summary>
|
||||
/// Will be called every animations of the hit and place block
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue