mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Added Book Support
This commit is contained in:
parent
8ba95c6140
commit
c94be261fd
15 changed files with 1275 additions and 3 deletions
293
MinecraftClient/Commands/Book.cs
Normal file
293
MinecraftClient/Commands/Book.cs
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Brigadier.NET;
|
||||||
|
using Brigadier.NET.Builder;
|
||||||
|
using MinecraftClient.CommandHandler;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
using MinecraftClient.Tui;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Commands
|
||||||
|
{
|
||||||
|
public class Book : Command
|
||||||
|
{
|
||||||
|
private const char PageDelimiter = '\f';
|
||||||
|
|
||||||
|
public override string CmdName => "book";
|
||||||
|
public override string CmdUsage => Translations.cmd_book_usage;
|
||||||
|
public override string CmdDesc => Translations.cmd_book_desc;
|
||||||
|
|
||||||
|
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||||
|
{
|
||||||
|
dispatcher.Register(l => l.Literal("help")
|
||||||
|
.Then(l => l.Literal(CmdName)
|
||||||
|
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||||
|
.Then(l => l.Literal("read").Executes(r => GetUsage(r.Source, "read")))
|
||||||
|
.Then(l => l.Literal("write").Executes(r => GetUsage(r.Source, "write")))
|
||||||
|
.Then(l => l.Literal("edit").Executes(r => GetUsage(r.Source, "edit")))
|
||||||
|
.Then(l => l.Literal("sign").Executes(r => GetUsage(r.Source, "sign")))));
|
||||||
|
|
||||||
|
dispatcher.Register(l => l.Literal(CmdName)
|
||||||
|
.Then(l => l.Literal("read")
|
||||||
|
.Executes(r => ReadBook(r.Source, null))
|
||||||
|
.Then(l => l.Argument("Page", Arguments.Integer(min: 1))
|
||||||
|
.Executes(r => ReadBook(r.Source, Arguments.GetInteger(r, "Page")))))
|
||||||
|
.Then(l => l.Literal("write")
|
||||||
|
.Then(l => l.Literal("text")
|
||||||
|
.Then(l => l.Argument("Text", Arguments.GreedyString())
|
||||||
|
.Executes(r => WriteBook(r.Source, Arguments.GetString(r, "Text")))))
|
||||||
|
.Then(l => l.Literal("file")
|
||||||
|
.Then(l => l.Argument("Path", Arguments.GreedyString())
|
||||||
|
.Executes(r => WriteBookFromFile(r.Source, Arguments.GetString(r, "Path"))))))
|
||||||
|
.Then(l => l.Literal("edit")
|
||||||
|
.Executes(r => OpenEditor(r.Source))
|
||||||
|
.Then(l => l.Literal("page")
|
||||||
|
.Then(l => l.Argument("Page", Arguments.Integer(min: 1))
|
||||||
|
.Then(l => l.Argument("Text", Arguments.GreedyString())
|
||||||
|
.Executes(r => EditPage(r.Source, Arguments.GetInteger(r, "Page"), Arguments.GetString(r, "Text"))))))
|
||||||
|
.Then(l => l.Literal("insert")
|
||||||
|
.Then(l => l.Argument("Page", Arguments.Integer(min: 1))
|
||||||
|
.Then(l => l.Argument("Text", Arguments.GreedyString())
|
||||||
|
.Executes(r => InsertPage(r.Source, Arguments.GetInteger(r, "Page"), Arguments.GetString(r, "Text"))))))
|
||||||
|
.Then(l => l.Literal("delete")
|
||||||
|
.Then(l => l.Argument("Page", Arguments.Integer(min: 1))
|
||||||
|
.Executes(r => DeletePage(r.Source, Arguments.GetInteger(r, "Page"))))))
|
||||||
|
.Then(l => l.Literal("sign")
|
||||||
|
.Then(l => l.Argument("Title", Arguments.GreedyString())
|
||||||
|
.Executes(r => SignBook(r.Source, Arguments.GetString(r, "Title")))))
|
||||||
|
.Then(l => l.Literal("_help")
|
||||||
|
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||||
|
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetUsage(CmdResult r, string? cmd)
|
||||||
|
{
|
||||||
|
return r.SetAndReturn(cmd switch
|
||||||
|
{
|
||||||
|
"read" => Translations.cmd_book_help_read,
|
||||||
|
"write" => Translations.cmd_book_help_write,
|
||||||
|
"edit" => Translations.cmd_book_help_edit,
|
||||||
|
"sign" => Translations.cmd_book_help_sign,
|
||||||
|
_ => GetCmdDescTranslated()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ReadBook(CmdResult r, int? page)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureInventory(r, handler))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (!handler.TryGetHeldBookContent(out BookContent content))
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_not_holding_book);
|
||||||
|
|
||||||
|
if (page is null && BookTuiHost.TryOpen(handler, BookHand.Main, editable: false))
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_tui_opened);
|
||||||
|
|
||||||
|
handler.Log.Info(FormatBook(content, page));
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Done);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int OpenEditor(CmdResult r)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureWritable(r, handler, out _))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return BookTuiHost.TryOpen(handler, BookHand.Main, editable: true)
|
||||||
|
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_tui_opened)
|
||||||
|
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_tui_required);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int WriteBook(CmdResult r, string text)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureWritable(r, handler, out _))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
IReadOnlyList<string> pages = SplitPages(text);
|
||||||
|
if (!Validate(r, handler, pages, title: null))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return handler.SendBookEdit(pages)
|
||||||
|
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_write_sent)
|
||||||
|
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_write_failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int WriteBookFromFile(CmdResult r, string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_file_not_found, path));
|
||||||
|
|
||||||
|
return WriteBook(r, File.ReadAllText(path, Encoding.UTF8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int EditPage(CmdResult r, int page, string text)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureWritable(r, handler, out BookContent content))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
List<string> pages = content.Pages.ToList();
|
||||||
|
if (page > pages.Count)
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_page_out_of_range, page, pages.Count));
|
||||||
|
|
||||||
|
pages[page - 1] = DecodeInlineText(text);
|
||||||
|
if (!Validate(r, handler, pages, title: null))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return handler.SendBookEdit(pages)
|
||||||
|
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_edit_sent)
|
||||||
|
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_write_failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int InsertPage(CmdResult r, int page, string text)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureWritable(r, handler, out BookContent content))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
List<string> pages = content.Pages.ToList();
|
||||||
|
if (page > pages.Count + 1)
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_page_out_of_range, page, pages.Count));
|
||||||
|
|
||||||
|
pages.Insert(page - 1, DecodeInlineText(text));
|
||||||
|
if (!Validate(r, handler, pages, title: null))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return handler.SendBookEdit(pages)
|
||||||
|
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_edit_sent)
|
||||||
|
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_write_failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int DeletePage(CmdResult r, int page)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureWritable(r, handler, out BookContent content))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
List<string> pages = content.Pages.ToList();
|
||||||
|
if (page > pages.Count)
|
||||||
|
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_page_out_of_range, page, pages.Count));
|
||||||
|
|
||||||
|
pages.RemoveAt(page - 1);
|
||||||
|
if (pages.Count == 0)
|
||||||
|
pages.Add(string.Empty);
|
||||||
|
|
||||||
|
if (!Validate(r, handler, pages, title: null))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return handler.SendBookEdit(pages)
|
||||||
|
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_edit_sent)
|
||||||
|
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_write_failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int SignBook(CmdResult r, string title)
|
||||||
|
{
|
||||||
|
McClient handler = CmdResult.currentHandler!;
|
||||||
|
if (!EnsureWritable(r, handler, out BookContent content))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
string normalizedTitle = title.Trim();
|
||||||
|
if (!Validate(r, handler, content.Pages, normalizedTitle))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return handler.SendBookEdit(content.Pages, normalizedTitle)
|
||||||
|
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_book_sign_sent)
|
||||||
|
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_write_failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EnsureInventory(CmdResult r, McClient handler)
|
||||||
|
{
|
||||||
|
if (handler.GetInventoryEnabled())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
r.SetAndReturn(CmdResult.Status.FailNeedInventory);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EnsureWritable(CmdResult r, McClient handler, out BookContent content)
|
||||||
|
{
|
||||||
|
content = BookContent.EmptyWritable;
|
||||||
|
if (!EnsureInventory(r, handler))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Item? item = handler.GetHeldBook();
|
||||||
|
if (!BookContentHelper.IsWritableBook(item))
|
||||||
|
{
|
||||||
|
r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_book_not_holding_writable);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BookContentHelper.TryRead(item, out content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> SplitPages(string text)
|
||||||
|
{
|
||||||
|
return BookContentHelper.NormalizePages(DecodeInlineText(text).Split(PageDelimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DecodeInlineText(string text)
|
||||||
|
{
|
||||||
|
return text.Replace("\\f", PageDelimiter.ToString(), StringComparison.Ordinal)
|
||||||
|
.Replace("\\n", "\n", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Validate(CmdResult r, McClient handler, IReadOnlyList<string> pages, string? title)
|
||||||
|
{
|
||||||
|
BookLimits limits = BookLimits.ForProtocol(handler.GetProtocolVersion());
|
||||||
|
|
||||||
|
if (pages.Count > limits.MaxPages)
|
||||||
|
{
|
||||||
|
r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_too_many_pages, pages.Count, limits.MaxPages));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < pages.Count; i++)
|
||||||
|
{
|
||||||
|
if (pages[i].Length > limits.MaxPageLength)
|
||||||
|
{
|
||||||
|
r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_page_too_long, i + 1, pages[i].Length, limits.MaxPageLength));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title is not null && (title.Length == 0 || title.Length > limits.MaxTitleLength))
|
||||||
|
{
|
||||||
|
r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_book_title_invalid, limits.MaxTitleLength));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatBook(BookContent content, int? page)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.AppendLine(content.IsSigned
|
||||||
|
? string.Format(Translations.cmd_book_header_signed, content.Title ?? string.Empty, content.Author ?? string.Empty)
|
||||||
|
: Translations.cmd_book_header_writable);
|
||||||
|
|
||||||
|
if (page is not null)
|
||||||
|
{
|
||||||
|
int index = page.Value - 1;
|
||||||
|
if (index < 0 || index >= content.Pages.Count)
|
||||||
|
return string.Format(Translations.cmd_book_page_out_of_range, page.Value, content.Pages.Count);
|
||||||
|
|
||||||
|
sb.AppendLine(string.Format(Translations.cmd_book_page_header, page.Value, content.Pages.Count));
|
||||||
|
sb.Append(content.Pages[index]);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < content.Pages.Count; i++)
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Format(Translations.cmd_book_page_header, i + 1, content.Pages.Count));
|
||||||
|
sb.AppendLine(content.Pages[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
MinecraftClient/Inventory/BookContent.cs
Normal file
183
MinecraftClient/Inventory/BookContent.cs
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using MinecraftClient.Protocol.Handlers;
|
||||||
|
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
|
||||||
|
using MinecraftClient.Protocol.Message;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
public enum BookHand
|
||||||
|
{
|
||||||
|
Main = 0,
|
||||||
|
Off = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record BookLimits(int MaxPages, int MaxPageLength, int MaxTitleLength)
|
||||||
|
{
|
||||||
|
public static BookLimits ForProtocol(int protocolVersion)
|
||||||
|
{
|
||||||
|
int maxPageLength = protocolVersion switch
|
||||||
|
{
|
||||||
|
>= Protocol18Handler.MC_1_21_2_Version => 1024,
|
||||||
|
>= Protocol18Handler.MC_1_17_Version => 8192,
|
||||||
|
_ => 32767
|
||||||
|
};
|
||||||
|
|
||||||
|
int maxTitleLength = protocolVersion switch
|
||||||
|
{
|
||||||
|
>= Protocol18Handler.MC_1_21_2_Version => 32,
|
||||||
|
>= Protocol18Handler.MC_1_17_Version => 128,
|
||||||
|
_ => 16
|
||||||
|
};
|
||||||
|
|
||||||
|
return new BookLimits(100, maxPageLength, maxTitleLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record BookContent(
|
||||||
|
IReadOnlyList<string> Pages,
|
||||||
|
string? Title,
|
||||||
|
string? Author,
|
||||||
|
int Generation,
|
||||||
|
bool IsSigned)
|
||||||
|
{
|
||||||
|
public static BookContent EmptyWritable { get; } = new([string.Empty], null, null, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BookContentHelper
|
||||||
|
{
|
||||||
|
public static bool IsBook(Item? item) => item?.Type is ItemType.WritableBook or ItemType.WrittenBook;
|
||||||
|
|
||||||
|
public static bool IsWritableBook(Item? item) => item?.Type == ItemType.WritableBook;
|
||||||
|
|
||||||
|
public static bool TryRead(Item? item, out BookContent content)
|
||||||
|
{
|
||||||
|
content = BookContent.EmptyWritable;
|
||||||
|
|
||||||
|
if (item is null || item.IsEmpty)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return item.Type switch
|
||||||
|
{
|
||||||
|
ItemType.WritableBook => TryReadWritable(item, out content),
|
||||||
|
ItemType.WrittenBook => TryReadWritten(item, out content),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Item CreateWritablePayload(Item currentBook, IReadOnlyList<string> pages)
|
||||||
|
{
|
||||||
|
return new Item(ItemType.WritableBook, 1, currentBook.Data, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["pages"] = pages.Cast<object>().ToArray()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Item CreateWrittenPayload(Item currentBook, IReadOnlyList<string> pages, string title, string author, bool encodePagesAsJson)
|
||||||
|
{
|
||||||
|
object[] encodedPages = pages
|
||||||
|
.Select(page => encodePagesAsJson ? ToJsonTextComponent(page) : page)
|
||||||
|
.Cast<object>()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new Item(ItemType.WrittenBook, 1, currentBook.Data, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["author"] = author,
|
||||||
|
["title"] = title,
|
||||||
|
["pages"] = encodedPages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> NormalizePages(IEnumerable<string> pages)
|
||||||
|
{
|
||||||
|
string[] normalized = pages.Select(page => page ?? string.Empty).ToArray();
|
||||||
|
return normalized.Length == 0 ? [string.Empty] : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadWritable(Item item, out BookContent content)
|
||||||
|
{
|
||||||
|
if (item.Components is not null)
|
||||||
|
{
|
||||||
|
var component = item.Components.OfType<WritableBlookContentComponent>().FirstOrDefault();
|
||||||
|
if (component is not null)
|
||||||
|
{
|
||||||
|
content = new BookContent(
|
||||||
|
NormalizePages(component.Pages.Select(page => page.RawContent)),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
IsSigned: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content = new BookContent(ReadStringList(item.NBT, "pages", parseJson: false), null, null, 0, IsSigned: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadWritten(Item item, out BookContent content)
|
||||||
|
{
|
||||||
|
if (item.Components is not null)
|
||||||
|
{
|
||||||
|
var component = item.Components.OfType<WrittenBlookContentComponent>().FirstOrDefault();
|
||||||
|
if (component is not null)
|
||||||
|
{
|
||||||
|
content = new BookContent(
|
||||||
|
NormalizePages(component.Pages.Select(page => page.RawContent)),
|
||||||
|
component.RawTitle,
|
||||||
|
component.Author,
|
||||||
|
component.Generation,
|
||||||
|
IsSigned: true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string? title = ReadString(item.NBT, "title");
|
||||||
|
string? author = ReadString(item.NBT, "author");
|
||||||
|
int generation = ReadInt(item.NBT, "generation");
|
||||||
|
content = new BookContent(ReadStringList(item.NBT, "pages", parseJson: true), title, author, generation, IsSigned: true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ReadStringList(Dictionary<string, object>? nbt, string key, bool parseJson)
|
||||||
|
{
|
||||||
|
if (nbt is null || !nbt.TryGetValue(key, out object? value) || value is not object[] values)
|
||||||
|
return [string.Empty];
|
||||||
|
|
||||||
|
string[] pages = values
|
||||||
|
.Select(value => value?.ToString() ?? string.Empty)
|
||||||
|
.Select(value => parseJson ? ChatParser.ParseText(value) : value)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return pages.Length == 0 ? [string.Empty] : pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ReadString(Dictionary<string, object>? nbt, string key)
|
||||||
|
{
|
||||||
|
return nbt is not null && nbt.TryGetValue(key, out object? value)
|
||||||
|
? value?.ToString()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadInt(Dictionary<string, object>? nbt, string key)
|
||||||
|
{
|
||||||
|
if (nbt is null || !nbt.TryGetValue(key, out object? value) || value is null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
int i => i,
|
||||||
|
short s => s,
|
||||||
|
byte b => b,
|
||||||
|
_ when int.TryParse(value.ToString(), out int parsed) => parsed,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToJsonTextComponent(string text)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(new Dictionary<string, string> { ["text"] = text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -272,4 +272,4 @@ namespace MinecraftClient.Inventory
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
MinecraftClient/Inventory/ItemPalettes/ItemPalette113.cs
Normal file
22
MinecraftClient/Inventory/ItemPalettes/ItemPalette113.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Inventory.ItemPalettes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal flattened item palette for 1.13-1.14 book handling.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemPalette113 : ItemPalette
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<int, ItemType> mappings = new()
|
||||||
|
{
|
||||||
|
[0] = ItemType.Air,
|
||||||
|
[687] = ItemType.WritableBook,
|
||||||
|
[688] = ItemType.WrittenBook
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override Dictionary<int, ItemType> GetDict()
|
||||||
|
{
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
MinecraftClient/Inventory/ItemPalettes/ItemPalette1132.cs
Normal file
22
MinecraftClient/Inventory/ItemPalettes/ItemPalette1132.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Inventory.ItemPalettes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal flattened item palette for 1.13.2 book handling.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemPalette1132 : ItemPalette
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<int, ItemType> mappings = new()
|
||||||
|
{
|
||||||
|
[0] = ItemType.Air,
|
||||||
|
[692] = ItemType.WritableBook,
|
||||||
|
[693] = ItemType.WrittenBook
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override Dictionary<int, ItemType> GetDict()
|
||||||
|
{
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
MinecraftClient/Inventory/ItemPalettes/ItemPalette114.cs
Normal file
22
MinecraftClient/Inventory/ItemPalettes/ItemPalette114.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Inventory.ItemPalettes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal flattened item palette for 1.14 book handling.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemPalette114 : ItemPalette
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<int, ItemType> mappings = new()
|
||||||
|
{
|
||||||
|
[0] = ItemType.Air,
|
||||||
|
[757] = ItemType.WritableBook,
|
||||||
|
[758] = ItemType.WrittenBook
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override Dictionary<int, ItemType> GetDict()
|
||||||
|
{
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1850,6 +1850,72 @@ namespace MinecraftClient
|
||||||
return handler.SendPluginChannelPacket(channel, data);
|
return handler.SendPluginChannelPacket(channel, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Item? GetHeldBook(BookHand hand = BookHand.Main)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
return InvokeOnMainThread(() => GetHeldBook(hand));
|
||||||
|
|
||||||
|
if (!inventoryHandlingEnabled || !inventories.TryGetValue(0, out Container? inventory))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int slot = hand == BookHand.Off ? 45 : 36 + CurrentSlot;
|
||||||
|
return inventory.Items.TryGetValue(slot, out Item? item) ? item : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetHeldBookContent(out BookContent content, BookHand hand = BookHand.Main)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
(bool ok, BookContent value) = InvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
bool ok = TryGetHeldBookContent(out BookContent value, hand);
|
||||||
|
return (ok, value);
|
||||||
|
});
|
||||||
|
content = value;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BookContentHelper.TryRead(GetHeldBook(hand), out content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendBookEdit(IReadOnlyList<string> pages, string? title = null)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
return InvokeOnMainThread(() => SendBookEdit(pages, title));
|
||||||
|
|
||||||
|
Item? currentBook = GetHeldBook(BookHand.Main);
|
||||||
|
if (!BookContentHelper.IsWritableBook(currentBook))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
IReadOnlyList<string> normalizedPages = BookContentHelper.NormalizePages(pages);
|
||||||
|
bool sent = handler.SendEditBook(currentBook!, normalizedPages, title, username, CurrentSlot);
|
||||||
|
if (sent && GetProtocolVersion() < Protocol18Handler.MC_1_17_Version)
|
||||||
|
SetHeldBook(BookHand.Main, CreateLocalBookResult(currentBook!, normalizedPages, title));
|
||||||
|
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Item CreateLocalBookResult(Item currentBook, IReadOnlyList<string> pages, string? title)
|
||||||
|
{
|
||||||
|
return title is null
|
||||||
|
? BookContentHelper.CreateWritablePayload(currentBook, pages)
|
||||||
|
: BookContentHelper.CreateWrittenPayload(
|
||||||
|
currentBook,
|
||||||
|
pages,
|
||||||
|
title,
|
||||||
|
username,
|
||||||
|
encodePagesAsJson: GetProtocolVersion() < Protocol18Handler.MC_1_9_Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetHeldBook(BookHand hand, Item item)
|
||||||
|
{
|
||||||
|
if (!inventoryHandlingEnabled || !inventories.TryGetValue(0, out Container? inventory))
|
||||||
|
return;
|
||||||
|
|
||||||
|
int slot = hand == BookHand.Off ? 45 : 36 + CurrentSlot;
|
||||||
|
inventory.Items[slot] = item;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send the Entity Action packet with the Specified ID
|
/// Send the Entity Action packet with the Specified ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -3817,6 +3883,17 @@ namespace MinecraftClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnBookOpen(int hand)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
InvokeOnMainThread(() => OnBookOpen(hand));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tui.BookTuiHost.OpenFromServer(this, hand == (int)BookHand.Off ? BookHand.Off : BookHand.Main);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when an entity spawned
|
/// Called when an entity spawned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -514,7 +514,7 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
var type = itemPalette.FromId(itemId);
|
var type = itemPalette.FromId(itemId);
|
||||||
itemCount = ReadNextByte(cache);
|
itemCount = ReadNextByte(cache);
|
||||||
nbt = ReadNextNbt(cache);
|
nbt = ReadNextNbt(cache);
|
||||||
return new Item(type, itemCount, nbt);
|
return new Item(type, itemCount, itemId, nbt);
|
||||||
}
|
}
|
||||||
case >= Protocol18Handler.MC_1_13_Version:
|
case >= Protocol18Handler.MC_1_13_Version:
|
||||||
{
|
{
|
||||||
|
|
@ -526,7 +526,7 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
var type = itemPalette.FromId(itemId);
|
var type = itemPalette.FromId(itemId);
|
||||||
itemCount = ReadNextByte(cache);
|
itemCount = ReadNextByte(cache);
|
||||||
nbt = ReadNextNbt(cache);
|
nbt = ReadNextNbt(cache);
|
||||||
return new Item(type, itemCount, nbt);
|
return new Item(type, itemCount, itemId, nbt);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -816,6 +816,11 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
return false; //MC 1.8-1.12.1 recipe book not supported
|
return false; //MC 1.8-1.12.1 recipe book not supported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SendEditBook(Item currentBook, IReadOnlyList<string> pages, string? title, string author, int selectedHotbarSlot)
|
||||||
|
{
|
||||||
|
return false; //MC 1.4.6-1.6.4 book editing is not supported
|
||||||
|
}
|
||||||
|
|
||||||
public bool SendCloseWindow(int windowId)
|
public bool SendCloseWindow(int windowId)
|
||||||
{
|
{
|
||||||
return false; //Currently not implemented
|
return false; //Currently not implemented
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,9 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
>= MC_1_16_2_Version => new ItemPalette1162(),
|
>= MC_1_16_2_Version => new ItemPalette1162(),
|
||||||
>= MC_1_16_1_Version => new ItemPalette1161(),
|
>= MC_1_16_1_Version => new ItemPalette1161(),
|
||||||
>= MC_1_15_Version => new ItemPalette115(),
|
>= MC_1_15_Version => new ItemPalette115(),
|
||||||
|
>= MC_1_14_Version => new ItemPalette114(),
|
||||||
|
>= MC_1_13_2_Version => new ItemPalette1132(),
|
||||||
|
>= MC_1_13_Version => new ItemPalette113(),
|
||||||
>= MC_1_12_Version => new ItemPalette112(),
|
>= MC_1_12_Version => new ItemPalette112(),
|
||||||
>= MC_1_11_Version => new ItemPalette111(),
|
>= MC_1_11_Version => new ItemPalette111(),
|
||||||
>= MC_1_10_Version => new ItemPalette110(),
|
>= MC_1_10_Version => new ItemPalette110(),
|
||||||
|
|
@ -2313,8 +2316,13 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
// Length is unneeded as the whole remaining packetData is the entire payload of the packet.
|
// Length is unneeded as the whole remaining packetData is the entire payload of the packet.
|
||||||
if (protocolVersion < MC_1_8_Version)
|
if (protocolVersion < MC_1_8_Version)
|
||||||
pForge.ReadNextVarShort(packetData);
|
pForge.ReadNextVarShort(packetData);
|
||||||
|
if (IsOpenBookPluginChannel(channel))
|
||||||
|
handler.OnBookOpen(ReadBookHand(new Queue<byte>(packetData)));
|
||||||
handler.OnPluginChannelMessage(channel, packetData.ToArray());
|
handler.OnPluginChannelMessage(channel, packetData.ToArray());
|
||||||
return pForge.HandlePluginMessage(channel, packetData, ref currentDimension);
|
return pForge.HandlePluginMessage(channel, packetData, ref currentDimension);
|
||||||
|
case PacketTypesIn.OpenBook:
|
||||||
|
handler.OnBookOpen(ReadBookHand(packetData));
|
||||||
|
break;
|
||||||
case PacketTypesIn.Disconnect:
|
case PacketTypesIn.Disconnect:
|
||||||
handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick,
|
handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick,
|
||||||
dataTypes.ReadNextChat(packetData));
|
dataTypes.ReadNextChat(packetData));
|
||||||
|
|
@ -2984,6 +2992,9 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
soundName = null;
|
soundName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (protocolVersion < MC_1_19_Version && packetData.Count < 21)
|
||||||
|
break;
|
||||||
|
|
||||||
int category = dataTypes.ReadNextVarInt(packetData);
|
int category = dataTypes.ReadNextVarInt(packetData);
|
||||||
double x = dataTypes.ReadNextInt(packetData) / 8.0D;
|
double x = dataTypes.ReadNextInt(packetData) / 8.0D;
|
||||||
double y = dataTypes.ReadNextInt(packetData) / 8.0D;
|
double y = dataTypes.ReadNextInt(packetData) / 8.0D;
|
||||||
|
|
@ -5279,6 +5290,18 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsOpenBookPluginChannel(string channel)
|
||||||
|
{
|
||||||
|
return protocolVersion < MC_1_13_Version
|
||||||
|
? string.Equals(channel, "MC|BOpen", StringComparison.Ordinal)
|
||||||
|
: string.Equals(channel, "minecraft:book_open", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ReadBookHand(Queue<byte> packetData)
|
||||||
|
{
|
||||||
|
return packetData.Count > 0 ? dataTypes.ReadNextVarInt(packetData) : (int)BookHand.Main;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a Login Plugin Response packet (0x02)
|
/// Send a Login Plugin Response packet (0x02)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -5881,6 +5904,80 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SendEditBook(Item currentBook, IReadOnlyList<string> pages, string? title, string author, int selectedHotbarSlot)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (protocolVersion < MC_1_8_Version)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool signing = title is not null;
|
||||||
|
IReadOnlyList<string> normalizedPages = BookContentHelper.NormalizePages(pages);
|
||||||
|
|
||||||
|
if (protocolVersion < MC_1_13_Version)
|
||||||
|
{
|
||||||
|
Item payload = signing
|
||||||
|
? BookContentHelper.CreateWrittenPayload(
|
||||||
|
currentBook,
|
||||||
|
normalizedPages,
|
||||||
|
title ?? string.Empty,
|
||||||
|
author,
|
||||||
|
encodePagesAsJson: protocolVersion < MC_1_9_Version)
|
||||||
|
: BookContentHelper.CreateWritablePayload(currentBook, normalizedPages);
|
||||||
|
|
||||||
|
byte[] payloadData = dataTypes.GetItemSlot(payload, itemPalette);
|
||||||
|
if (payloadData.Length > 32767)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return SendPluginChannelPacket(signing ? "MC|BSign" : "MC|BEdit", payloadData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocolVersion < MC_1_17_Version)
|
||||||
|
{
|
||||||
|
Item payload = signing
|
||||||
|
? BookContentHelper.CreateWrittenPayload(currentBook, normalizedPages, title ?? string.Empty, author, encodePagesAsJson: false)
|
||||||
|
: BookContentHelper.CreateWritablePayload(currentBook, normalizedPages);
|
||||||
|
|
||||||
|
List<byte> packet = new();
|
||||||
|
packet.AddRange(dataTypes.GetItemSlot(payload, itemPalette));
|
||||||
|
packet.AddRange(dataTypes.GetBool(signing));
|
||||||
|
|
||||||
|
if (protocolVersion >= MC_1_16_5_Version)
|
||||||
|
packet.AddRange(DataTypes.GetVarInt(selectedHotbarSlot));
|
||||||
|
else if (protocolVersion >= MC_1_13_2_Version)
|
||||||
|
packet.AddRange(DataTypes.GetVarInt((int)BookHand.Main));
|
||||||
|
|
||||||
|
SendPacket(PacketTypesOut.EditBook, packet);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<byte> modernPacket = new();
|
||||||
|
modernPacket.AddRange(DataTypes.GetVarInt(selectedHotbarSlot));
|
||||||
|
modernPacket.AddRange(DataTypes.GetVarInt(normalizedPages.Count));
|
||||||
|
foreach (string page in normalizedPages)
|
||||||
|
modernPacket.AddRange(dataTypes.GetString(page));
|
||||||
|
|
||||||
|
modernPacket.AddRange(dataTypes.GetBool(signing));
|
||||||
|
if (signing)
|
||||||
|
modernPacket.AddRange(dataTypes.GetString(title ?? string.Empty));
|
||||||
|
|
||||||
|
SendPacket(PacketTypesOut.EditBook, modernPacket);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (System.IO.IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool SendAnimation(int animation, int playerId)
|
public bool SendAnimation(int animation, int playerId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,17 @@ namespace MinecraftClient.Protocol
|
||||||
/// <returns>True if packet was successfully sent</returns>
|
/// <returns>True if packet was successfully sent</returns>
|
||||||
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
|
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a book edit/sign packet for the currently held writable book.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentBook">Current held writable book</param>
|
||||||
|
/// <param name="pages">Book pages</param>
|
||||||
|
/// <param name="title">Title when signing, otherwise null</param>
|
||||||
|
/// <param name="author">Current player name when signing</param>
|
||||||
|
/// <param name="selectedHotbarSlot">Selected hotbar slot, 0-8</param>
|
||||||
|
/// <returns>True if packet was successfully sent</returns>
|
||||||
|
bool SendEditBook(Item currentBook, IReadOnlyList<string> pages, string? title, string author, int selectedHotbarSlot);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plays animation
|
/// Plays animation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,12 @@ namespace MinecraftClient.Protocol
|
||||||
/// <param name="data">The data from the channel</param>
|
/// <param name="data">The data from the channel</param>
|
||||||
void OnPluginChannelMessage(string channel, byte[] data);
|
void OnPluginChannelMessage(string channel, byte[] data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the server asks the client to open a book UI.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hand">Book hand, 0 main hand, 1 off hand.</param>
|
||||||
|
void OnBookOpen(int hand);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when an entity has spawned
|
/// Called when an entity has spawned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -7552,5 +7552,149 @@ namespace MinecraftClient {
|
||||||
return ResourceManager.GetString("cmd.achievement.entry", resourceCulture);
|
return ResourceManager.GetString("cmd.achievement.entry", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal static string cmd_book_usage {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.usage", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_desc {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.desc", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_help_read {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.help_read", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_help_write {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.help_write", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_help_edit {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.help_edit", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_help_sign {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.help_sign", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_not_holding_book {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.not_holding_book", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_not_holding_writable {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.not_holding_writable", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_tui_opened {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.tui_opened", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_tui_required {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.tui_required", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_write_sent {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.write_sent", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_edit_sent {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.edit_sent", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_sign_sent {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.sign_sent", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_write_failed {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.write_failed", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_file_not_found {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.file_not_found", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_page_out_of_range {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.page_out_of_range", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_too_many_pages {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.too_many_pages", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_page_too_long {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.page_too_long", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_title_invalid {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.title_invalid", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_header_signed {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.header_signed", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_header_writable {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.header_writable", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string cmd_book_page_header {
|
||||||
|
get { return ResourceManager.GetString("cmd.book.page_header", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_title_watermark {
|
||||||
|
get { return ResourceManager.GetString("tui.book.title_watermark", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_prev {
|
||||||
|
get { return ResourceManager.GetString("tui.book.prev", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_next {
|
||||||
|
get { return ResourceManager.GetString("tui.book.next", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_insert {
|
||||||
|
get { return ResourceManager.GetString("tui.book.insert", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_delete {
|
||||||
|
get { return ResourceManager.GetString("tui.book.delete", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_save {
|
||||||
|
get { return ResourceManager.GetString("tui.book.save", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_sign {
|
||||||
|
get { return ResourceManager.GetString("tui.book.sign", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_close {
|
||||||
|
get { return ResourceManager.GetString("tui.book.close", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_saved {
|
||||||
|
get { return ResourceManager.GetString("tui.book.saved", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_save_failed {
|
||||||
|
get { return ResourceManager.GetString("tui.book.save_failed", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_signed {
|
||||||
|
get { return ResourceManager.GetString("tui.book.signed", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_page_header {
|
||||||
|
get { return ResourceManager.GetString("tui.book.page_header", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_editing {
|
||||||
|
get { return ResourceManager.GetString("tui.book.editing", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string tui_book_reading {
|
||||||
|
get { return ResourceManager.GetString("tui.book.reading", resourceCulture); }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2659,4 +2659,112 @@ see item details.</value>
|
||||||
<data name="cmd.achievement.entry" xml:space="preserve">
|
<data name="cmd.achievement.entry" xml:space="preserve">
|
||||||
<value>{0} {1} [{2}]</value>
|
<value>{0} {1} [{2}]</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="cmd.book.usage" xml:space="preserve">
|
||||||
|
<value>book <read|write|edit|sign></value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.desc" xml:space="preserve">
|
||||||
|
<value>read, write, edit, and sign the book held in your main hand.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.help_read" xml:space="preserve">
|
||||||
|
<value>/book read [page]</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.help_write" xml:space="preserve">
|
||||||
|
<value>/book write text <text> or /book write file <path>. Use \n for line breaks and \f for page breaks.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.help_edit" xml:space="preserve">
|
||||||
|
<value>/book edit, /book edit page <page> <text>, /book edit insert <page> <text>, or /book edit delete <page>.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.help_sign" xml:space="preserve">
|
||||||
|
<value>/book sign <title></value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.not_holding_book" xml:space="preserve">
|
||||||
|
<value>You are not holding a book.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.not_holding_writable" xml:space="preserve">
|
||||||
|
<value>You must hold a writable book in your main hand.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.tui_opened" xml:space="preserve">
|
||||||
|
<value>Book TUI opened.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.tui_required" xml:space="preserve">
|
||||||
|
<value>The book editor is only available in TUI mode.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.write_sent" xml:space="preserve">
|
||||||
|
<value>Book write packet sent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.edit_sent" xml:space="preserve">
|
||||||
|
<value>Book edit packet sent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.sign_sent" xml:space="preserve">
|
||||||
|
<value>Book sign packet sent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.write_failed" xml:space="preserve">
|
||||||
|
<value>Book packet could not be sent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.file_not_found" xml:space="preserve">
|
||||||
|
<value>Book file not found: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.page_out_of_range" xml:space="preserve">
|
||||||
|
<value>Page {0} is outside the current page range 1-{1}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.too_many_pages" xml:space="preserve">
|
||||||
|
<value>Book has {0} pages, but this version supports at most {1} pages.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.page_too_long" xml:space="preserve">
|
||||||
|
<value>Page {0} has {1} characters, but this version supports at most {2} characters per page.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.title_invalid" xml:space="preserve">
|
||||||
|
<value>Book title must be 1-{0} characters.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.header_signed" xml:space="preserve">
|
||||||
|
<value>Written book: {0} by {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.header_writable" xml:space="preserve">
|
||||||
|
<value>Writable book</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.book.page_header" xml:space="preserve">
|
||||||
|
<value>Page {0}/{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.title_watermark" xml:space="preserve">
|
||||||
|
<value>Title</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.prev" xml:space="preserve">
|
||||||
|
<value>Prev</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.next" xml:space="preserve">
|
||||||
|
<value>Next</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.insert" xml:space="preserve">
|
||||||
|
<value>Insert</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.delete" xml:space="preserve">
|
||||||
|
<value>Delete</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.sign" xml:space="preserve">
|
||||||
|
<value>Sign</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.close" xml:space="preserve">
|
||||||
|
<value>Close</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.saved" xml:space="preserve">
|
||||||
|
<value>Saved.</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.save_failed" xml:space="preserve">
|
||||||
|
<value>Book packet could not be sent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.signed" xml:space="preserve">
|
||||||
|
<value>Signed.</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.page_header" xml:space="preserve">
|
||||||
|
<value>Book page {0}/{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.editing" xml:space="preserve">
|
||||||
|
<value>Editing writable book.</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.book.reading" xml:space="preserve">
|
||||||
|
<value>Reading book.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
||||||
282
MinecraftClient/Tui/BookTuiHost.cs
Normal file
282
MinecraftClient/Tui/BookTuiHost.cs
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui;
|
||||||
|
|
||||||
|
public static class BookTuiHost
|
||||||
|
{
|
||||||
|
private static volatile bool isRunning;
|
||||||
|
|
||||||
|
public static bool TryOpen(McClient handler, BookHand hand, bool editable)
|
||||||
|
{
|
||||||
|
if (ConsoleIO.Backend is not TuiConsoleBackend)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Open(handler, hand, editable);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void OpenFromServer(McClient handler, BookHand hand)
|
||||||
|
{
|
||||||
|
if (ConsoleIO.Backend is TuiConsoleBackend)
|
||||||
|
Open(handler, hand, editable: BookContentHelper.IsWritableBook(handler.GetHeldBook(hand)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Open(McClient handler, BookHand hand, bool editable)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (isRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MainTuiView? view = TuiConsoleBackend.Instance?.GetView();
|
||||||
|
if (view is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!handler.TryGetHeldBookContent(out BookContent content, hand))
|
||||||
|
return;
|
||||||
|
|
||||||
|
isRunning = true;
|
||||||
|
var bookView = new BookView(handler, content, editable && !content.IsSigned);
|
||||||
|
view.ShowOverlay(bookView, () => isRunning = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class BookView : UserControl
|
||||||
|
{
|
||||||
|
private readonly McClient handler;
|
||||||
|
private readonly bool editable;
|
||||||
|
private readonly List<string> pages;
|
||||||
|
private readonly TextBlock header;
|
||||||
|
private readonly TextBlock status;
|
||||||
|
private readonly TextBox pageText;
|
||||||
|
private readonly TextBox titleText;
|
||||||
|
private int pageIndex;
|
||||||
|
|
||||||
|
public BookView(McClient handler, BookContent content, bool editable)
|
||||||
|
{
|
||||||
|
this.handler = handler;
|
||||||
|
this.editable = editable;
|
||||||
|
pages = content.Pages.ToList();
|
||||||
|
if (pages.Count == 0)
|
||||||
|
pages.Add(string.Empty);
|
||||||
|
|
||||||
|
Focusable = true;
|
||||||
|
Background = Brushes.Black;
|
||||||
|
|
||||||
|
header = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = Brushes.Yellow,
|
||||||
|
Margin = new Thickness(1, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
};
|
||||||
|
|
||||||
|
status = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = Brushes.Gray,
|
||||||
|
Margin = new Thickness(1, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
};
|
||||||
|
|
||||||
|
pageText = new TextBox
|
||||||
|
{
|
||||||
|
AcceptsReturn = true,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
IsReadOnly = !editable,
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
Background = Brushes.Black,
|
||||||
|
BorderBrush = Brushes.Gray,
|
||||||
|
MinHeight = 12,
|
||||||
|
Margin = new Thickness(1)
|
||||||
|
};
|
||||||
|
pageText.TextChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (editable && pageIndex >= 0 && pageIndex < pages.Count)
|
||||||
|
pages[pageIndex] = pageText.Text ?? string.Empty;
|
||||||
|
};
|
||||||
|
|
||||||
|
titleText = new TextBox
|
||||||
|
{
|
||||||
|
Watermark = Translations.tui_book_title_watermark,
|
||||||
|
IsVisible = editable,
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
Background = Brushes.Black,
|
||||||
|
BorderBrush = Brushes.Gray,
|
||||||
|
Margin = new Thickness(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
var controls = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Spacing = 1,
|
||||||
|
Margin = new Thickness(1),
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
Button(Translations.tui_book_prev, (_, _) => MovePage(-1)),
|
||||||
|
Button(Translations.tui_book_next, (_, _) => MovePage(1)),
|
||||||
|
Button(Translations.tui_book_insert, (_, _) => InsertPage(), editable),
|
||||||
|
Button(Translations.tui_book_delete, (_, _) => DeletePage(), editable),
|
||||||
|
Button(Translations.tui_book_save, (_, _) => Save(), editable),
|
||||||
|
Button(Translations.tui_book_sign, (_, _) => Sign(), editable),
|
||||||
|
Button(Translations.tui_book_close, (_, _) => Close())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var panel = new DockPanel
|
||||||
|
{
|
||||||
|
Background = Brushes.Black,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
DockTo(header, Dock.Top),
|
||||||
|
DockTo(status, Dock.Bottom),
|
||||||
|
DockTo(controls, Dock.Bottom),
|
||||||
|
DockTo(titleText, Dock.Bottom),
|
||||||
|
pageText
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Content = panel;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.PageUp)
|
||||||
|
{
|
||||||
|
MovePage(-1);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Key == Key.PageDown)
|
||||||
|
{
|
||||||
|
MovePage(1);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Control DockTo(Control control, Dock dock)
|
||||||
|
{
|
||||||
|
DockPanel.SetDock(control, dock);
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button Button(string text, EventHandler<Avalonia.Interactivity.RoutedEventArgs> handler, bool enabled = true)
|
||||||
|
{
|
||||||
|
var button = new Button
|
||||||
|
{
|
||||||
|
Content = text,
|
||||||
|
IsEnabled = enabled,
|
||||||
|
Padding = new Thickness(1, 0),
|
||||||
|
Margin = new Thickness(0)
|
||||||
|
};
|
||||||
|
button.Click += handler;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MovePage(int delta)
|
||||||
|
{
|
||||||
|
pageIndex = Math.Clamp(pageIndex + delta, 0, pages.Count - 1);
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertPage()
|
||||||
|
{
|
||||||
|
pages.Insert(pageIndex + 1, string.Empty);
|
||||||
|
pageIndex++;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeletePage()
|
||||||
|
{
|
||||||
|
if (pages.Count == 1)
|
||||||
|
pages[0] = string.Empty;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pages.RemoveAt(pageIndex);
|
||||||
|
pageIndex = Math.Clamp(pageIndex, 0, pages.Count - 1);
|
||||||
|
}
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if (!Validate(out string error))
|
||||||
|
{
|
||||||
|
status.Text = error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Text = handler.SendBookEdit(pages)
|
||||||
|
? Translations.tui_book_saved
|
||||||
|
: Translations.tui_book_save_failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Sign()
|
||||||
|
{
|
||||||
|
string title = (titleText.Text ?? string.Empty).Trim();
|
||||||
|
if (!Validate(out string error, title))
|
||||||
|
{
|
||||||
|
status.Text = error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Text = handler.SendBookEdit(pages, title)
|
||||||
|
? Translations.tui_book_signed
|
||||||
|
: Translations.tui_book_save_failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Validate(out string error, string? title = null)
|
||||||
|
{
|
||||||
|
BookLimits limits = BookLimits.ForProtocol(handler.GetProtocolVersion());
|
||||||
|
error = string.Empty;
|
||||||
|
|
||||||
|
if (pages.Count > limits.MaxPages)
|
||||||
|
{
|
||||||
|
error = string.Format(Translations.cmd_book_too_many_pages, pages.Count, limits.MaxPages);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < pages.Count; i++)
|
||||||
|
{
|
||||||
|
if (pages[i].Length > limits.MaxPageLength)
|
||||||
|
{
|
||||||
|
error = string.Format(Translations.cmd_book_page_too_long, i + 1, pages[i].Length, limits.MaxPageLength);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title is not null && (title.Length == 0 || title.Length > limits.MaxTitleLength))
|
||||||
|
{
|
||||||
|
error = string.Format(Translations.cmd_book_title_invalid, limits.MaxTitleLength);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
TuiConsoleBackend.Instance?.GetView()?.HideOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Refresh()
|
||||||
|
{
|
||||||
|
pageText.Text = pages[pageIndex];
|
||||||
|
header.Text = string.Format(Translations.tui_book_page_header, pageIndex + 1, pages.Count);
|
||||||
|
status.Text = editable ? Translations.tui_book_editing : Translations.tui_book_reading;
|
||||||
|
pageText.Focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue