mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge pull request #3011: TUI support for more container
This commit is contained in:
commit
5270aed315
22 changed files with 2119 additions and 847 deletions
|
|
@ -435,7 +435,7 @@ namespace MinecraftClient.Commands
|
||||||
return r.SetAndReturn(CmdResult.Status.Fail, msg);
|
return r.SetAndReturn(CmdResult.Status.Fail, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.Type != ContainerType.PlayerInventory)
|
if (!Tui.ContainerViewBase.HasTuiSupport(container.Type))
|
||||||
{
|
{
|
||||||
handler.Log.Warn(string.Format(Translations.cmd_inventory_tui_unsupported_container, inventoryId));
|
handler.Log.Warn(string.Format(Translations.cmd_inventory_tui_unsupported_container, inventoryId));
|
||||||
return r.SetAndReturn(CmdResult.Status.Fail);
|
return r.SetAndReturn(CmdResult.Status.Fail);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MinecraftClient.Inventory
|
namespace MinecraftClient.Inventory
|
||||||
{
|
{
|
||||||
|
|
@ -91,10 +91,11 @@ namespace MinecraftClient.Inventory
|
||||||
/// <param name="id">Container ID</param>
|
/// <param name="id">Container ID</param>
|
||||||
/// <param name="typeID">Container Type</param>
|
/// <param name="typeID">Container Type</param>
|
||||||
/// <param name="title">Container Title</param>
|
/// <param name="title">Container Title</param>
|
||||||
public Container(int id, int typeID, string title)
|
/// <param name="protocolVersion">Protocol version for version-specific mapping</param>
|
||||||
|
public Container(int id, int typeID, string title, int protocolVersion = 0)
|
||||||
{
|
{
|
||||||
ID = id;
|
ID = id;
|
||||||
Type = GetContainerType(typeID);
|
Type = GetContainerType(typeID, protocolVersion);
|
||||||
Title = title;
|
Title = title;
|
||||||
Items = new();
|
Items = new();
|
||||||
Properties = new();
|
Properties = new();
|
||||||
|
|
@ -131,22 +132,62 @@ namespace MinecraftClient.Inventory
|
||||||
/// Get container type from Type ID
|
/// Get container type from Type ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="typeID">Container Type ID</param>
|
/// <param name="typeID">Container Type ID</param>
|
||||||
|
/// <param name="protocolVersion">Protocol version (menu registry changed across versions)</param>
|
||||||
/// <returns>Container Type</returns>
|
/// <returns>Container Type</returns>
|
||||||
public static ContainerType GetContainerType(int typeID)
|
public static ContainerType GetContainerType(int typeID, int protocolVersion = 0)
|
||||||
{
|
{
|
||||||
// https://wiki.vg/Inventory didn't state the inventory ID, assume that list start with 0
|
// MC 1.20.4 (protocol 765) added crafter_3x3 at index 7, shifting all subsequent IDs by +1.
|
||||||
|
// Registry order from decompiled MenuType.java:
|
||||||
|
// 1.14-1.20.2: generic_9x1..generic_3x3(6), anvil(7), beacon(8), ... stonecutter(22)
|
||||||
|
// 1.20.4+: generic_9x1..generic_3x3(6), crafter_3x3(7), anvil(8), beacon(9), ... stonecutter(24)
|
||||||
|
if (protocolVersion >= 765)
|
||||||
|
{
|
||||||
|
return typeID switch
|
||||||
|
{
|
||||||
|
#pragma warning disable format // @formatter:off
|
||||||
|
0 => ContainerType.Generic_9x1,
|
||||||
|
1 => ContainerType.Generic_9x2,
|
||||||
|
2 => ContainerType.Generic_9x3,
|
||||||
|
3 => ContainerType.Generic_9x4,
|
||||||
|
4 => ContainerType.Generic_9x5,
|
||||||
|
5 => ContainerType.Generic_9x6,
|
||||||
|
6 => ContainerType.Generic_3x3,
|
||||||
|
7 => ContainerType.Crafter,
|
||||||
|
8 => ContainerType.Anvil,
|
||||||
|
9 => ContainerType.Beacon,
|
||||||
|
10 => ContainerType.BlastFurnace,
|
||||||
|
11 => ContainerType.BrewingStand,
|
||||||
|
12 => ContainerType.Crafting,
|
||||||
|
13 => ContainerType.Enchantment,
|
||||||
|
14 => ContainerType.Furnace,
|
||||||
|
15 => ContainerType.Grindstone,
|
||||||
|
16 => ContainerType.Hopper,
|
||||||
|
17 => ContainerType.Lectern,
|
||||||
|
18 => ContainerType.Loom,
|
||||||
|
19 => ContainerType.Merchant,
|
||||||
|
20 => ContainerType.ShulkerBox,
|
||||||
|
21 => ContainerType.SmightingTable,
|
||||||
|
22 => ContainerType.Smoker,
|
||||||
|
23 => ContainerType.Cartography,
|
||||||
|
24 => ContainerType.Stonecutter,
|
||||||
|
_ => ContainerType.Unknown,
|
||||||
|
#pragma warning restore format // @formatter:on
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return typeID switch
|
return typeID switch
|
||||||
{
|
{
|
||||||
0 => ContainerType.Generic_9x1,
|
#pragma warning disable format // @formatter:off
|
||||||
1 => ContainerType.Generic_9x2,
|
0 => ContainerType.Generic_9x1,
|
||||||
2 => ContainerType.Generic_9x3,
|
1 => ContainerType.Generic_9x2,
|
||||||
3 => ContainerType.Generic_9x4,
|
2 => ContainerType.Generic_9x3,
|
||||||
4 => ContainerType.Generic_9x5,
|
3 => ContainerType.Generic_9x4,
|
||||||
5 => ContainerType.Generic_9x6,
|
4 => ContainerType.Generic_9x5,
|
||||||
6 => ContainerType.Generic_3x3,
|
5 => ContainerType.Generic_9x6,
|
||||||
7 => ContainerType.Anvil,
|
6 => ContainerType.Generic_3x3,
|
||||||
8 => ContainerType.Beacon,
|
7 => ContainerType.Anvil,
|
||||||
9 => ContainerType.BlastFurnace,
|
8 => ContainerType.Beacon,
|
||||||
|
9 => ContainerType.BlastFurnace,
|
||||||
10 => ContainerType.BrewingStand,
|
10 => ContainerType.BrewingStand,
|
||||||
11 => ContainerType.Crafting,
|
11 => ContainerType.Crafting,
|
||||||
12 => ContainerType.Enchantment,
|
12 => ContainerType.Enchantment,
|
||||||
|
|
@ -160,7 +201,8 @@ namespace MinecraftClient.Inventory
|
||||||
20 => ContainerType.Smoker,
|
20 => ContainerType.Smoker,
|
||||||
21 => ContainerType.Cartography,
|
21 => ContainerType.Cartography,
|
||||||
22 => ContainerType.Stonecutter,
|
22 => ContainerType.Stonecutter,
|
||||||
_ => ContainerType.Unknown,
|
_ => ContainerType.Unknown,
|
||||||
|
#pragma warning restore format // @formatter:on
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace MinecraftClient.Inventory
|
namespace MinecraftClient.Inventory
|
||||||
{
|
{
|
||||||
// For MC 1.14 after ONLY
|
// For MC 1.14 after ONLY
|
||||||
public enum ContainerType
|
public enum ContainerType
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
Generic_9x5,
|
Generic_9x5,
|
||||||
Generic_9x6,
|
Generic_9x6,
|
||||||
Generic_3x3,
|
Generic_3x3,
|
||||||
|
Crafter,
|
||||||
Anvil,
|
Anvil,
|
||||||
Beacon,
|
Beacon,
|
||||||
BlastFurnace,
|
BlastFurnace,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace MinecraftClient.Inventory
|
namespace MinecraftClient.Inventory
|
||||||
{
|
{
|
||||||
public static class ContainerTypeExtensions
|
public static class ContainerTypeExtensions
|
||||||
{
|
{
|
||||||
|
|
@ -13,9 +13,14 @@
|
||||||
{
|
{
|
||||||
#pragma warning disable format // @formatter:off
|
#pragma warning disable format // @formatter:off
|
||||||
ContainerType.PlayerInventory => 46,
|
ContainerType.PlayerInventory => 46,
|
||||||
|
ContainerType.Generic_9x1 => 45,
|
||||||
|
ContainerType.Generic_9x2 => 54,
|
||||||
ContainerType.Generic_9x3 => 63,
|
ContainerType.Generic_9x3 => 63,
|
||||||
|
ContainerType.Generic_9x4 => 72,
|
||||||
|
ContainerType.Generic_9x5 => 81,
|
||||||
ContainerType.Generic_9x6 => 90,
|
ContainerType.Generic_9x6 => 90,
|
||||||
ContainerType.Generic_3x3 => 45,
|
ContainerType.Generic_3x3 => 45,
|
||||||
|
ContainerType.Crafter => 45,
|
||||||
ContainerType.Crafting => 46,
|
ContainerType.Crafting => 46,
|
||||||
ContainerType.BlastFurnace => 39,
|
ContainerType.BlastFurnace => 39,
|
||||||
ContainerType.Furnace => 39,
|
ContainerType.Furnace => 39,
|
||||||
|
|
@ -27,6 +32,7 @@
|
||||||
ContainerType.Anvil => 39,
|
ContainerType.Anvil => 39,
|
||||||
ContainerType.Hopper => 41,
|
ContainerType.Hopper => 41,
|
||||||
ContainerType.ShulkerBox => 63,
|
ContainerType.ShulkerBox => 63,
|
||||||
|
ContainerType.SmightingTable => 39,
|
||||||
ContainerType.Loom => 40,
|
ContainerType.Loom => 40,
|
||||||
ContainerType.Stonecutter => 38,
|
ContainerType.Stonecutter => 38,
|
||||||
ContainerType.Lectern => 37,
|
ContainerType.Lectern => 37,
|
||||||
|
|
@ -52,6 +58,7 @@
|
||||||
ContainerType.Generic_9x3 => AsciiArt.Container_Generic_9x3,
|
ContainerType.Generic_9x3 => AsciiArt.Container_Generic_9x3,
|
||||||
ContainerType.Generic_9x6 => AsciiArt.Container_Generic_9x6,
|
ContainerType.Generic_9x6 => AsciiArt.Container_Generic_9x6,
|
||||||
ContainerType.Generic_3x3 => AsciiArt.Container_Generic_3x3,
|
ContainerType.Generic_3x3 => AsciiArt.Container_Generic_3x3,
|
||||||
|
ContainerType.Crafter => AsciiArt.Container_Generic_3x3,
|
||||||
ContainerType.Crafting => AsciiArt.Container_Crafting,
|
ContainerType.Crafting => AsciiArt.Container_Crafting,
|
||||||
ContainerType.BlastFurnace => AsciiArt.Container_Furnace,
|
ContainerType.BlastFurnace => AsciiArt.Container_Furnace,
|
||||||
ContainerType.Furnace => AsciiArt.Container_Furnace,
|
ContainerType.Furnace => AsciiArt.Container_Furnace,
|
||||||
|
|
|
||||||
|
|
@ -3124,6 +3124,13 @@ namespace MinecraftClient
|
||||||
Log.Info(string.Format(Translations.extra_inventory_open, inventoryID, inventory.Title));
|
Log.Info(string.Format(Translations.extra_inventory_open, inventoryID, inventory.Title));
|
||||||
Log.Info(Translations.extra_inventory_interact);
|
Log.Info(Translations.extra_inventory_interact);
|
||||||
DispatchBotEvent(bot => bot.OnInventoryOpen(inventoryID));
|
DispatchBotEvent(bot => bot.OnInventoryOpen(inventoryID));
|
||||||
|
|
||||||
|
if (ConsoleIO.Backend is Tui.TuiConsoleBackend
|
||||||
|
&& Tui.ContainerViewBase.HasTuiSupport(inventory.Type)
|
||||||
|
&& Tui.InventoryTuiHost.CanLaunch)
|
||||||
|
{
|
||||||
|
Tui.InventoryTuiHost.Launch(this, inventoryID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3146,6 +3153,8 @@ namespace MinecraftClient
|
||||||
Log.Info(string.Format(Translations.extra_inventory_close, inventoryID));
|
Log.Info(string.Format(Translations.extra_inventory_close, inventoryID));
|
||||||
DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID));
|
DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Tui.InventoryTuiHost.NotifyInventoryClosed(inventoryID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1715,16 +1715,15 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
public byte[] GetLocation(Location location)
|
public byte[] GetLocation(Location location)
|
||||||
{
|
{
|
||||||
byte[] locationBytes;
|
byte[] locationBytes;
|
||||||
|
ulong x = (ulong)(int)Math.Floor(location.X) & 0x3FFFFFF;
|
||||||
|
ulong y = (ulong)(int)Math.Floor(location.Y) & 0xFFF;
|
||||||
|
ulong z = (ulong)(int)Math.Floor(location.Z) & 0x3FFFFFF;
|
||||||
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
|
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
|
||||||
{
|
{
|
||||||
locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) |
|
locationBytes = BitConverter.GetBytes((x << 38) | (z << 12) | y);
|
||||||
((((ulong)location.Z) & 0x3FFFFFF) << 12) |
|
|
||||||
(((ulong)location.Y) & 0xFFF));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) |
|
locationBytes = BitConverter.GetBytes((x << 38) | (y << 26) | z);
|
||||||
((((ulong)location.Y) & 0xFFF) << 26) |
|
|
||||||
(((ulong)location.Z) & 0x3FFFFFF));
|
|
||||||
|
|
||||||
Array.Reverse(locationBytes); //Endianness
|
Array.Reverse(locationBytes); //Endianness
|
||||||
return locationBytes;
|
return locationBytes;
|
||||||
|
|
|
||||||
|
|
@ -2319,7 +2319,7 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
var windowId = dataTypes.ReadNextVarInt(packetData);
|
var windowId = dataTypes.ReadNextVarInt(packetData);
|
||||||
var windowType = dataTypes.ReadNextVarInt(packetData);
|
var windowType = dataTypes.ReadNextVarInt(packetData);
|
||||||
var title = dataTypes.ReadNextChat(packetData);
|
var title = dataTypes.ReadNextChat(packetData);
|
||||||
Container inventory = new(windowId, windowType, ChatParser.ParseText(title));
|
Container inventory = new(windowId, windowType, ChatParser.ParseText(title), protocolVersion);
|
||||||
handler.OnInventoryOpen(windowId, inventory);
|
handler.OnInventoryOpen(windowId, inventory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6530,6 +6530,15 @@ namespace MinecraftClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Durability.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_inventory_durability {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.inventory.durability", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Container not found.
|
/// Looks up a localized string similar to Container not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -6664,5 +6673,131 @@ namespace MinecraftClient {
|
||||||
return ResourceManager.GetString("effect.name.with_amplifier", resourceCulture);
|
return ResourceManager.GetString("effect.name.with_amplifier", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Container.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_container_label {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.container.label", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Input.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_furnace_input {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.furnace.input", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Fuel.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_furnace_fuel {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.furnace.fuel", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Output.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_furnace_output {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.furnace.output", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Item.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_enchanting_item {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.enchanting.item", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Lapis.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_enchanting_lapis {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.enchanting.lapis", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Enchant Options.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_enchanting_options {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.enchanting.options", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Option {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_enchanting_option_slot {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.enchanting.option_slot", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Fuel.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_brewing_fuel {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.brewing.fuel", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Ingredient.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_brewing_ingredient {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.brewing.ingredient", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Bottle {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_brewing_bottle {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.brewing.bottle", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Input 1.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_grindstone_input1 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.grindstone.input1", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Input 2.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_grindstone_input2 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.grindstone.input2", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Crafting.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_crafting_grid {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.crafting.grid", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2302,6 +2302,9 @@ see item details.</value>
|
||||||
<data name="tui.inventory.slot_detail" xml:space="preserve">
|
<data name="tui.inventory.slot_detail" xml:space="preserve">
|
||||||
<value>Slot #{0} Count: {1}</value>
|
<value>Slot #{0} Count: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="tui.inventory.durability" xml:space="preserve">
|
||||||
|
<value>Durability</value>
|
||||||
|
</data>
|
||||||
<data name="tui.inventory.container_not_found" xml:space="preserve">
|
<data name="tui.inventory.container_not_found" xml:space="preserve">
|
||||||
<value>Container not found</value>
|
<value>Container not found</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -2347,4 +2350,46 @@ see item details.</value>
|
||||||
<data name="effect.name.with_amplifier" xml:space="preserve">
|
<data name="effect.name.with_amplifier" xml:space="preserve">
|
||||||
<value>{0} {1}</value>
|
<value>{0} {1}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="tui.container.label" xml:space="preserve">
|
||||||
|
<value>Container</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.furnace.input" xml:space="preserve">
|
||||||
|
<value>Input</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.furnace.fuel" xml:space="preserve">
|
||||||
|
<value>Fuel</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.furnace.output" xml:space="preserve">
|
||||||
|
<value>Output</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.enchanting.item" xml:space="preserve">
|
||||||
|
<value>Item</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.enchanting.lapis" xml:space="preserve">
|
||||||
|
<value>Lapis</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.enchanting.options" xml:space="preserve">
|
||||||
|
<value>Enchant Options</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.enchanting.option_slot" xml:space="preserve">
|
||||||
|
<value>Option {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.brewing.fuel" xml:space="preserve">
|
||||||
|
<value>Fuel</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.brewing.ingredient" xml:space="preserve">
|
||||||
|
<value>Ingredient</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.brewing.bottle" xml:space="preserve">
|
||||||
|
<value>Bottle {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.grindstone.input1" xml:space="preserve">
|
||||||
|
<value>Input 1</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.grindstone.input2" xml:space="preserve">
|
||||||
|
<value>Input 2</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.crafting.grid" xml:space="preserve">
|
||||||
|
<value>Crafting</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
152
MinecraftClient/Tui/BrewingStandView.cs
Normal file
152
MinecraftClient/Tui/BrewingStandView.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class BrewingStandView : ContainerViewBase
|
||||||
|
{
|
||||||
|
private readonly BrewingViewModel _brewVm;
|
||||||
|
|
||||||
|
public BrewingStandView(McClient handler, int windowId)
|
||||||
|
: base(new BrewingViewModel(handler, windowId))
|
||||||
|
{
|
||||||
|
_brewVm = (BrewingViewModel)_vm;
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return 3 + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 0,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
var topRow = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Spacing = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var fuelCol = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
fuelCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_brewing_fuel,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
fuelCol.Children.Add(CreateSlotCell(_brewVm.FuelSlot, 0, 0));
|
||||||
|
topRow.Children.Add(fuelCol);
|
||||||
|
|
||||||
|
topRow.Children.Add(new Border { Width = 2 });
|
||||||
|
|
||||||
|
var ingredientCol = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
ingredientCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_brewing_ingredient,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
ingredientCol.Children.Add(CreateSlotCell(_brewVm.IngredientSlot, 0, 1));
|
||||||
|
topRow.Children.Add(ingredientCol);
|
||||||
|
|
||||||
|
panel.Children.Add(topRow);
|
||||||
|
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\u25bc",
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
var bottleRow = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Spacing = 0,
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var bottlePanel = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
bottlePanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = string.Format(Translations.tui_brewing_bottle, i + 1),
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
bottlePanel.Children.Add(CreateSlotCell(_brewVm.BottleSlots[i], 1, i));
|
||||||
|
bottleRow.Children.Add(bottlePanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.Children.Add(bottleRow);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BrewingViewModel : ContainerViewModel
|
||||||
|
{
|
||||||
|
public ObservableCollection<SlotViewModel> BottleSlots { get; } = new();
|
||||||
|
public SlotViewModel IngredientSlot { get; private set; } = null!;
|
||||||
|
public SlotViewModel FuelSlot { get; private set; } = null!;
|
||||||
|
|
||||||
|
public BrewingViewModel(McClient handler, int windowId)
|
||||||
|
: base(handler, windowId, ContainerType.BrewingStand)
|
||||||
|
{
|
||||||
|
IngredientSlot = SlotMap[3];
|
||||||
|
FuelSlot = SlotMap[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeSlots()
|
||||||
|
{
|
||||||
|
SlotMap.Clear();
|
||||||
|
|
||||||
|
for (int i = 0; i <= 2; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
BottleSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlotMap[3] = new SlotViewModel(3);
|
||||||
|
SlotMap[4] = new SlotViewModel(4);
|
||||||
|
|
||||||
|
for (int i = 5; i <= 31; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
MainInventorySlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 32; i <= 40; i++)
|
||||||
|
{
|
||||||
|
int hotbarIdx = i - 32;
|
||||||
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
|
HotbarSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
730
MinecraftClient/Tui/ContainerViewBase.cs
Normal file
730
MinecraftClient/Tui/ContainerViewBase.cs
Normal file
|
|
@ -0,0 +1,730 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public abstract class ContainerViewBase : UserControl
|
||||||
|
{
|
||||||
|
protected static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40));
|
||||||
|
protected static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55));
|
||||||
|
protected static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75));
|
||||||
|
protected static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90));
|
||||||
|
protected static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140));
|
||||||
|
protected static readonly IBrush BrName = Brushes.White;
|
||||||
|
protected static readonly IBrush BrCount = Brushes.Yellow;
|
||||||
|
protected static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80));
|
||||||
|
protected static readonly IBrush BrEquipLbl = Brushes.DarkCyan;
|
||||||
|
protected static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60));
|
||||||
|
protected static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80));
|
||||||
|
protected static readonly IBrush BrHeldItemBorder = Brushes.Yellow;
|
||||||
|
|
||||||
|
protected int _slotW;
|
||||||
|
protected int _slotH;
|
||||||
|
protected int _nameMaxLen;
|
||||||
|
protected int _nameLines;
|
||||||
|
protected int _termW;
|
||||||
|
|
||||||
|
protected readonly ContainerViewModel _vm;
|
||||||
|
protected TextBlock _titleText = null!;
|
||||||
|
protected Border _infoDetailBorder = null!;
|
||||||
|
protected TextBlock _infoDetailText = null!;
|
||||||
|
protected TextBlock _cursorItemText = null!;
|
||||||
|
protected TextBlock _helpText = null!;
|
||||||
|
|
||||||
|
protected TextBlock[] _hotbarIndicators = new TextBlock[9];
|
||||||
|
protected int _currentHotbarSlot = -1;
|
||||||
|
|
||||||
|
protected Border? _lastHoveredSlotBorder;
|
||||||
|
|
||||||
|
protected Canvas _overlayCanvas = null!;
|
||||||
|
protected Border _heldItemFloater = null!;
|
||||||
|
protected TextBlock _heldItemFloaterName = null!;
|
||||||
|
protected TextBlock _heldItemFloaterCount = null!;
|
||||||
|
|
||||||
|
protected ScrollViewer _chatScrollViewer = null!;
|
||||||
|
protected ObservableCollection<string>? _chatLines;
|
||||||
|
protected int _lastTermW;
|
||||||
|
protected int _lastTermH;
|
||||||
|
protected bool _chatScrollToBottom = true;
|
||||||
|
|
||||||
|
protected ContainerViewBase(ContainerViewModel vm)
|
||||||
|
{
|
||||||
|
_vm = vm;
|
||||||
|
_currentHotbarSlot = vm.Handler.GetCurrentSlot();
|
||||||
|
|
||||||
|
_chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50)
|
||||||
|
?? new ObservableCollection<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Initialize()
|
||||||
|
{
|
||||||
|
RebuildUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract int GetTotalSlotRows();
|
||||||
|
|
||||||
|
protected abstract Control BuildContainerSpecificArea();
|
||||||
|
|
||||||
|
protected virtual void OnContainerDataChanged() { }
|
||||||
|
|
||||||
|
protected virtual void RebuildUi()
|
||||||
|
{
|
||||||
|
int termH;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_termW = System.Console.WindowWidth;
|
||||||
|
termH = System.Console.WindowHeight;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_termW = 120;
|
||||||
|
termH = 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastTermW = _termW;
|
||||||
|
_lastTermH = termH;
|
||||||
|
|
||||||
|
int availW = _termW - 26;
|
||||||
|
_slotW = Math.Clamp(availW / 9, 8, 18);
|
||||||
|
_nameMaxLen = _slotW;
|
||||||
|
|
||||||
|
int totalRows = GetTotalSlotRows();
|
||||||
|
int overhead = 4;
|
||||||
|
int chatMinH = 1;
|
||||||
|
_slotH = Math.Clamp((termH - overhead - chatMinH) / totalRows, 2, 5);
|
||||||
|
_nameLines = _slotH;
|
||||||
|
|
||||||
|
_vm.SetSlotDisplayParams(_nameMaxLen, _nameLines);
|
||||||
|
|
||||||
|
_lastHoveredSlotBorder = null;
|
||||||
|
|
||||||
|
_titleText = new TextBlock
|
||||||
|
{
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
Foreground = Brushes.Cyan,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
_infoDetailText = new TextBlock
|
||||||
|
{
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
};
|
||||||
|
|
||||||
|
_infoDetailBorder = new Border
|
||||||
|
{
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Child = _infoDetailText,
|
||||||
|
};
|
||||||
|
|
||||||
|
_cursorItemText = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = Brushes.Yellow,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
};
|
||||||
|
|
||||||
|
_helpText = new TextBlock
|
||||||
|
{
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
||||||
|
Text = Translations.tui_inventory_controls_help,
|
||||||
|
};
|
||||||
|
|
||||||
|
_heldItemFloaterName = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
};
|
||||||
|
_heldItemFloaterCount = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = BrCount,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
};
|
||||||
|
_heldItemFloater = new Border
|
||||||
|
{
|
||||||
|
Background = BrHeldItemBg,
|
||||||
|
BorderBrush = BrHeldItemBorder,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Padding = new Thickness(1, 0),
|
||||||
|
IsVisible = false,
|
||||||
|
MaxWidth = 24,
|
||||||
|
Child = new StackPanel
|
||||||
|
{
|
||||||
|
Children = { _heldItemFloaterName, _heldItemFloaterCount },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
_overlayCanvas = new Canvas { IsHitTestVisible = false };
|
||||||
|
_overlayCanvas.Children.Add(_heldItemFloater);
|
||||||
|
|
||||||
|
var chatLines = _chatLines!;
|
||||||
|
chatLines.CollectionChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var sv = _chatScrollViewer;
|
||||||
|
if (sv.Extent.Height > sv.Viewport.Height)
|
||||||
|
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||||
|
}, Avalonia.Threading.DispatcherPriority.Background);
|
||||||
|
};
|
||||||
|
var chatItemsControl = new ItemsControl
|
||||||
|
{
|
||||||
|
ItemsSource = chatLines,
|
||||||
|
Focusable = false,
|
||||||
|
ItemTemplate = new FuncDataTemplate<string>((s, _) =>
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = s,
|
||||||
|
Foreground = Brushes.Gray,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = new Thickness(0),
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
_chatScrollViewer = new ScrollViewer
|
||||||
|
{
|
||||||
|
Content = chatItemsControl,
|
||||||
|
Background = Brushes.Black,
|
||||||
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||||
|
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
_hotbarIndicators = new TextBlock[9];
|
||||||
|
|
||||||
|
Content = BuildRootLayout();
|
||||||
|
UpdateTitle();
|
||||||
|
UpdateInfoPanel();
|
||||||
|
|
||||||
|
_chatScrollToBottom = true;
|
||||||
|
_chatScrollViewer.ScrollChanged += OnChatScrollChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_chatScrollToBottom) return;
|
||||||
|
var sv = _chatScrollViewer;
|
||||||
|
if (sv.Extent.Height > sv.Viewport.Height)
|
||||||
|
{
|
||||||
|
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||||
|
_chatScrollToBottom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Control BuildRootLayout()
|
||||||
|
{
|
||||||
|
var inventoryArea = BuildMainArea();
|
||||||
|
DockPanel.SetDock(_titleText, Dock.Top);
|
||||||
|
DockPanel.SetDock(inventoryArea, Dock.Top);
|
||||||
|
|
||||||
|
var mainContent = new DockPanel
|
||||||
|
{
|
||||||
|
Children = { _titleText, inventoryArea, _chatScrollViewer }
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Panel
|
||||||
|
{
|
||||||
|
Background = Brushes.Black,
|
||||||
|
Children = { mainContent, _overlayCanvas }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Control BuildMainArea()
|
||||||
|
{
|
||||||
|
var infoPanel = BuildInfoPanel();
|
||||||
|
DockPanel.SetDock(infoPanel, Dock.Right);
|
||||||
|
|
||||||
|
return new DockPanel
|
||||||
|
{
|
||||||
|
Children = { infoPanel, BuildInventoryPanel() }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Control BuildInventoryPanel()
|
||||||
|
{
|
||||||
|
var root = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 0,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
root.Children.Add(BuildContainerSpecificArea());
|
||||||
|
root.Children.Add(BuildSeparator());
|
||||||
|
root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9));
|
||||||
|
root.Children.Add(BuildHotbarSection());
|
||||||
|
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
BorderBrush = Brushes.Gray,
|
||||||
|
Child = root,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Control BuildSeparator()
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Height = 1,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
Margin = new Thickness(0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Control BuildInfoPanel()
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
BorderBrush = Brushes.Gray,
|
||||||
|
Padding = new Thickness(1),
|
||||||
|
Width = 24,
|
||||||
|
Child = new StackPanel
|
||||||
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan },
|
||||||
|
_infoDetailBorder,
|
||||||
|
new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) },
|
||||||
|
_cursorItemText,
|
||||||
|
new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) },
|
||||||
|
_helpText,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Control BuildHotbarSection()
|
||||||
|
{
|
||||||
|
var panel = new StackPanel { Spacing = 0 };
|
||||||
|
|
||||||
|
var numberRow = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 9; i++)
|
||||||
|
{
|
||||||
|
bool active = i == _currentHotbarSlot;
|
||||||
|
string label = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||||
|
|
||||||
|
var tb = new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
Width = _slotW,
|
||||||
|
TextAlignment = TextAlignment.Center,
|
||||||
|
Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
};
|
||||||
|
_hotbarIndicators[i] = tb;
|
||||||
|
numberRow.Children.Add(tb);
|
||||||
|
}
|
||||||
|
panel.Children.Add(numberRow);
|
||||||
|
panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9));
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Control BuildSlotGrid(ObservableCollection<SlotViewModel> slots, int columns)
|
||||||
|
{
|
||||||
|
var grid = new Grid();
|
||||||
|
int rows = (slots.Count + columns - 1) / columns;
|
||||||
|
|
||||||
|
for (int r = 0; r < rows; r++)
|
||||||
|
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
|
for (int c = 0; c < columns; c++)
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||||
|
|
||||||
|
for (int i = 0; i < slots.Count; i++)
|
||||||
|
{
|
||||||
|
int row = i / columns;
|
||||||
|
int col = i % columns;
|
||||||
|
var cell = CreateSlotCell(slots[i], row, col);
|
||||||
|
Grid.SetRow(cell, row);
|
||||||
|
Grid.SetColumn(cell, col);
|
||||||
|
grid.Children.Add(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static IBrush GetSlotBg(bool isEmpty, int row, int col)
|
||||||
|
{
|
||||||
|
bool isA = (row + col) % 2 == 0;
|
||||||
|
return isEmpty
|
||||||
|
? (isA ? BrSlotEmptyA : BrSlotEmptyB)
|
||||||
|
: (isA ? BrSlotFillA : BrSlotFillB);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0)
|
||||||
|
{
|
||||||
|
var nameTb = new TextBlock
|
||||||
|
{
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = new Thickness(0),
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
};
|
||||||
|
|
||||||
|
var countTb = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = BrCount,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = new Thickness(0),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = VerticalAlignment.Bottom,
|
||||||
|
};
|
||||||
|
|
||||||
|
ApplySlotVisual(slot, nameTb, countTb);
|
||||||
|
|
||||||
|
int r = row, c = col;
|
||||||
|
var border = new Border
|
||||||
|
{
|
||||||
|
Width = _slotW,
|
||||||
|
Height = _slotH,
|
||||||
|
Background = GetSlotBg(slot.IsEmpty, r, c),
|
||||||
|
Child = new Panel
|
||||||
|
{
|
||||||
|
Children = { nameTb, countTb },
|
||||||
|
},
|
||||||
|
Tag = (slot, r, c),
|
||||||
|
};
|
||||||
|
|
||||||
|
border.PointerPressed += OnSlotPointerPressed;
|
||||||
|
border.PointerEntered += OnSlotPointerEnter;
|
||||||
|
border.PointerExited += OnSlotPointerExit;
|
||||||
|
border.PointerMoved += OnSlotPointerMoved;
|
||||||
|
|
||||||
|
slot.PropertyChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
ApplySlotVisual(slot, nameTb, countTb);
|
||||||
|
border.Background = GetSlotBg(slot.IsEmpty, r, c);
|
||||||
|
};
|
||||||
|
|
||||||
|
return border;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb)
|
||||||
|
{
|
||||||
|
if (slot.IsEmpty)
|
||||||
|
{
|
||||||
|
nameTb.Text = "";
|
||||||
|
nameTb.Foreground = BrDim;
|
||||||
|
countTb.Text = "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nameTb.Text = slot.ItemDisplayText;
|
||||||
|
nameTb.Foreground = BrName;
|
||||||
|
countTb.Text = slot.CountDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextBlock MakeLabel(string text)
|
||||||
|
{
|
||||||
|
return new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(1, 0, 0, 0),
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Pointer / Keyboard interaction
|
||||||
|
|
||||||
|
private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetHover(border, slot);
|
||||||
|
|
||||||
|
var point = e.GetCurrentPoint(border);
|
||||||
|
bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0;
|
||||||
|
|
||||||
|
WindowActionType action;
|
||||||
|
if (point.Properties.IsRightButtonPressed)
|
||||||
|
action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick;
|
||||||
|
else
|
||||||
|
action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick;
|
||||||
|
|
||||||
|
_vm.PerformAction(slot.SlotId, action);
|
||||||
|
UpdateInfoPanel();
|
||||||
|
UpdateHeldItemFloater(e);
|
||||||
|
OnContainerDataChanged();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSlotPointerEnter(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||||
|
{
|
||||||
|
SetHover(b, slot);
|
||||||
|
UpdateHeldItemFloater(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSlotPointerMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||||
|
{
|
||||||
|
SetHover(b, slot);
|
||||||
|
UpdateHeldItemFloater(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSlotPointerExit(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col))
|
||||||
|
b.Background = GetSlotBg(slot.IsEmpty, row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetHover(Border border, SlotViewModel slot)
|
||||||
|
{
|
||||||
|
if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border)
|
||||||
|
{
|
||||||
|
if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc))
|
||||||
|
_lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastHoveredSlotBorder = border;
|
||||||
|
border.Background = BrSlotHover;
|
||||||
|
_vm.HoveredSlot = slot;
|
||||||
|
UpdateInfoPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateHeldItemFloater(PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_vm.HasCursorItem)
|
||||||
|
{
|
||||||
|
_heldItemFloater.IsVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_heldItemFloaterName.Text = _vm.CursorItemInfo;
|
||||||
|
_heldItemFloaterCount.Text = "";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pos = e.GetPosition(_overlayCanvas);
|
||||||
|
double left = pos.X + 2;
|
||||||
|
double remainingW = _termW - left - 2;
|
||||||
|
int maxW = Math.Max(8, (int)remainingW);
|
||||||
|
_heldItemFloater.MaxWidth = maxW;
|
||||||
|
Canvas.SetLeft(_heldItemFloater, left);
|
||||||
|
Canvas.SetTop(_heldItemFloater, pos.Y);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_heldItemFloater.MaxWidth = 24;
|
||||||
|
Canvas.SetLeft(_heldItemFloater, 0);
|
||||||
|
Canvas.SetTop(_heldItemFloater, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_heldItemFloater.IsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateInfoPanel()
|
||||||
|
{
|
||||||
|
_infoDetailText.Text = _vm.HoveredSlotDetailText;
|
||||||
|
|
||||||
|
bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty;
|
||||||
|
_infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent;
|
||||||
|
|
||||||
|
if (_vm.HasCursorItem)
|
||||||
|
{
|
||||||
|
_cursorItemText.Text = _vm.CursorItemInfo;
|
||||||
|
_cursorItemText.Foreground = Brushes.Yellow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cursorItemText.Text = Translations.tui_inventory_cursor_empty;
|
||||||
|
_cursorItemText.Foreground = BrDim;
|
||||||
|
_heldItemFloater.IsVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateTitle()
|
||||||
|
{
|
||||||
|
_titleText.Text = _vm.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CloseInventory()
|
||||||
|
{
|
||||||
|
if (_vm.WindowId != 0)
|
||||||
|
_vm.Handler.CloseInventory(_vm.WindowId);
|
||||||
|
|
||||||
|
if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend)
|
||||||
|
tuiBackend.GetView()?.HideOverlay();
|
||||||
|
else
|
||||||
|
(Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnKeyDown(e);
|
||||||
|
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Escape:
|
||||||
|
case Key.E:
|
||||||
|
CloseInventory();
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.C:
|
||||||
|
if ((e.KeyModifiers & KeyModifiers.Shift) != 0 &&
|
||||||
|
_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||||
|
{
|
||||||
|
_vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick);
|
||||||
|
UpdateInfoPanel();
|
||||||
|
}
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.Q:
|
||||||
|
if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||||
|
{
|
||||||
|
var action = (e.KeyModifiers & KeyModifiers.Control) != 0
|
||||||
|
? WindowActionType.DropItemStack
|
||||||
|
: WindowActionType.DropItem;
|
||||||
|
_vm.PerformAction(_vm.HoveredSlot.SlotId, action);
|
||||||
|
UpdateInfoPanel();
|
||||||
|
}
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.R:
|
||||||
|
_vm.RefreshFromContainer();
|
||||||
|
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||||
|
UpdateHotbarIndicators();
|
||||||
|
UpdateInfoPanel();
|
||||||
|
OnContainerDataChanged();
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateHotbarIndicators()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 9; i++)
|
||||||
|
{
|
||||||
|
bool active = i == _currentHotbarSlot;
|
||||||
|
_hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||||
|
_hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
Focusable = true;
|
||||||
|
Focus();
|
||||||
|
AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
||||||
|
SizeChanged += OnViewSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Escape)
|
||||||
|
{
|
||||||
|
CloseInventory();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
int newW, newH;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newW = System.Console.WindowWidth;
|
||||||
|
newH = System.Console.WindowHeight;
|
||||||
|
}
|
||||||
|
catch { return; }
|
||||||
|
|
||||||
|
if (newW == _lastTermW && newH == _lastTermH) return;
|
||||||
|
|
||||||
|
_vm.RefreshFromContainer();
|
||||||
|
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||||
|
RebuildUi();
|
||||||
|
Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnGotFocus(GotFocusEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnGotFocus(e);
|
||||||
|
Focusable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public static bool HasTuiSupport(ContainerType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
ContainerType.PlayerInventory => true,
|
||||||
|
ContainerType.Generic_9x1 => true,
|
||||||
|
ContainerType.Generic_9x2 => true,
|
||||||
|
ContainerType.Generic_9x3 => true,
|
||||||
|
ContainerType.Generic_9x4 => true,
|
||||||
|
ContainerType.Generic_9x5 => true,
|
||||||
|
ContainerType.Generic_9x6 => true,
|
||||||
|
ContainerType.Generic_3x3 => true,
|
||||||
|
ContainerType.Crafter => true,
|
||||||
|
ContainerType.ShulkerBox => true,
|
||||||
|
ContainerType.Crafting => true,
|
||||||
|
ContainerType.Furnace => true,
|
||||||
|
ContainerType.BlastFurnace => true,
|
||||||
|
ContainerType.Smoker => true,
|
||||||
|
ContainerType.Enchantment => true,
|
||||||
|
ContainerType.BrewingStand => true,
|
||||||
|
ContainerType.Hopper => true,
|
||||||
|
ContainerType.Grindstone => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContainerViewBase CreateView(ContainerType type, McClient handler, int windowId)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
ContainerType.PlayerInventory => new PlayerInventoryView(handler, windowId),
|
||||||
|
ContainerType.Generic_9x3 or ContainerType.ShulkerBox => new GridContainerView(handler, windowId, type, 3, 9),
|
||||||
|
ContainerType.Generic_9x6 => new GridContainerView(handler, windowId, type, 6, 9),
|
||||||
|
ContainerType.Generic_3x3 or ContainerType.Crafter
|
||||||
|
=> new GridContainerView(handler, windowId, type, 3, 3),
|
||||||
|
ContainerType.Generic_9x1 => new GridContainerView(handler, windowId, type, 1, 9),
|
||||||
|
ContainerType.Generic_9x2 => new GridContainerView(handler, windowId, type, 2, 9),
|
||||||
|
ContainerType.Generic_9x4 => new GridContainerView(handler, windowId, type, 4, 9),
|
||||||
|
ContainerType.Generic_9x5 => new GridContainerView(handler, windowId, type, 5, 9),
|
||||||
|
ContainerType.Crafting => new CraftingView(handler, windowId),
|
||||||
|
ContainerType.Furnace or ContainerType.BlastFurnace or ContainerType.Smoker
|
||||||
|
=> new FurnaceView(handler, windowId, type),
|
||||||
|
ContainerType.Enchantment => new EnchantingTableView(handler, windowId),
|
||||||
|
ContainerType.BrewingStand => new BrewingStandView(handler, windowId),
|
||||||
|
ContainerType.Hopper => new HopperView(handler, windowId),
|
||||||
|
ContainerType.Grindstone => new GrindstoneView(handler, windowId),
|
||||||
|
_ => throw new ArgumentException($"No TUI view for {type}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
271
MinecraftClient/Tui/ContainerViewModel.cs
Normal file
271
MinecraftClient/Tui/ContainerViewModel.cs
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class ContainerViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private SlotViewModel? _hoveredSlot;
|
||||||
|
private string _title = "";
|
||||||
|
private string _statusText = "";
|
||||||
|
private string _cursorItemInfo = "";
|
||||||
|
private bool _hasCursorItem;
|
||||||
|
|
||||||
|
public McClient Handler { get; }
|
||||||
|
public int WindowId { get; }
|
||||||
|
public ContainerType ContainerType { get; }
|
||||||
|
|
||||||
|
public ObservableCollection<SlotViewModel> ContainerSlots { get; } = new();
|
||||||
|
public ObservableCollection<SlotViewModel> MainInventorySlots { get; } = new();
|
||||||
|
public ObservableCollection<SlotViewModel> HotbarSlots { get; } = new();
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
set { _title = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StatusText
|
||||||
|
{
|
||||||
|
get => _statusText;
|
||||||
|
set { _statusText = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CursorItemInfo
|
||||||
|
{
|
||||||
|
get => _cursorItemInfo;
|
||||||
|
set { _cursorItemInfo = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasCursorItem
|
||||||
|
{
|
||||||
|
get => _hasCursorItem;
|
||||||
|
set { _hasCursorItem = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public SlotViewModel? HoveredSlot
|
||||||
|
{
|
||||||
|
get => _hoveredSlot;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_hoveredSlot != null)
|
||||||
|
_hoveredSlot.IsHovered = false;
|
||||||
|
_hoveredSlot = value;
|
||||||
|
if (_hoveredSlot != null)
|
||||||
|
_hoveredSlot.IsHovered = true;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string HoveredSlotDetailText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_hoveredSlot == null)
|
||||||
|
return Translations.tui_inventory_hover_hint;
|
||||||
|
|
||||||
|
if (_hoveredSlot.IsEmpty)
|
||||||
|
return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(_hoveredSlot.ItemTypeName);
|
||||||
|
sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount));
|
||||||
|
|
||||||
|
var item = _hoveredSlot.RawItem;
|
||||||
|
if (item != null)
|
||||||
|
AppendItemExtras(sb, item);
|
||||||
|
|
||||||
|
return sb.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Dictionary<int, SlotViewModel> SlotMap { get; } = new();
|
||||||
|
|
||||||
|
public ContainerViewModel(McClient handler, int windowId, ContainerType containerType)
|
||||||
|
{
|
||||||
|
Handler = handler;
|
||||||
|
WindowId = windowId;
|
||||||
|
ContainerType = containerType;
|
||||||
|
|
||||||
|
InitializeSlots();
|
||||||
|
RefreshFromContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSlotDisplayParams(int maxWidth, int maxLines)
|
||||||
|
{
|
||||||
|
foreach (var kvp in SlotMap)
|
||||||
|
{
|
||||||
|
kvp.Value.NameMaxWidth = maxWidth;
|
||||||
|
kvp.Value.NameMaxLines = maxLines;
|
||||||
|
}
|
||||||
|
RefreshFromContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void InitializeSlots()
|
||||||
|
{
|
||||||
|
SlotMap.Clear();
|
||||||
|
|
||||||
|
int slotCount = ContainerType.SlotCount();
|
||||||
|
if (slotCount == 0) return;
|
||||||
|
|
||||||
|
int playerInvStart = slotCount - 36;
|
||||||
|
|
||||||
|
for (int i = 0; i < playerInvStart; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
ContainerSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = playerInvStart; i < playerInvStart + 27; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
MainInventorySlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = playerInvStart + 27; i < slotCount; i++)
|
||||||
|
{
|
||||||
|
int hotbarIdx = i - (playerInvStart + 27);
|
||||||
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
|
HotbarSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void RefreshFromContainer()
|
||||||
|
{
|
||||||
|
Inventory.Container? container = Handler.GetInventory(WindowId);
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
StatusText = Translations.tui_inventory_container_not_found;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title);
|
||||||
|
|
||||||
|
foreach (var kvp in SlotMap)
|
||||||
|
{
|
||||||
|
Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null;
|
||||||
|
kvp.Value.Update(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCursorItem(container);
|
||||||
|
int itemCount = 0;
|
||||||
|
foreach (var kvp in container.Items)
|
||||||
|
{
|
||||||
|
if (kvp.Key >= 0 && !kvp.Value.IsEmpty)
|
||||||
|
itemCount++;
|
||||||
|
}
|
||||||
|
StatusText = string.Format(Translations.tui_inventory_item_count, itemCount);
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateCursorItem(Inventory.Container _)
|
||||||
|
{
|
||||||
|
var playerInv = Handler.GetInventory(0);
|
||||||
|
if (playerInv != null && playerInv.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty)
|
||||||
|
{
|
||||||
|
CursorItemInfo = FormatItemDetail(cursorItem);
|
||||||
|
HasCursorItem = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CursorItemInfo = "";
|
||||||
|
HasCursorItem = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static string FormatItemDetail(Item item)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"x{item.Count} {item.GetTypeString()}");
|
||||||
|
AppendItemExtras(sb, item);
|
||||||
|
if (sb.Length > 0 && sb[sb.Length - 1] == '\n')
|
||||||
|
sb.Remove(sb.Length - 1, 1);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendItemExtras(StringBuilder sb, Item item)
|
||||||
|
{
|
||||||
|
int damage = item.Damage;
|
||||||
|
if (damage != 0)
|
||||||
|
{
|
||||||
|
int maxDamage = item.Components?.OfType<MaxDamageComponent>().FirstOrDefault()?.MaxDamage ?? 0;
|
||||||
|
if (maxDamage > 0)
|
||||||
|
sb.AppendLine($"{Translations.tui_inventory_durability}: {maxDamage - damage}/{maxDamage}");
|
||||||
|
else
|
||||||
|
sb.AppendLine($"{Translations.cmd_inventory_damage}: {damage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var enchList = item.EnchantmentList;
|
||||||
|
if (enchList is not null)
|
||||||
|
{
|
||||||
|
bool isFirstEnchantment = true;
|
||||||
|
foreach (var ench in enchList)
|
||||||
|
{
|
||||||
|
string name = EnchantmentMapping.GetEnchantmentName(ench.Type);
|
||||||
|
string level = EnchantmentMapping.ConvertLevelToRomanNumbers(ench.Level);
|
||||||
|
if (isFirstEnchantment)
|
||||||
|
{
|
||||||
|
isFirstEnchantment = false;
|
||||||
|
sb.Append($"{name} {level}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append($" | {name} {level}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item.NBT is not null &&
|
||||||
|
(item.NBT.TryGetValue("Enchantments", out object? enchantments) ||
|
||||||
|
item.NBT.TryGetValue("StoredEnchantments", out enchantments)))
|
||||||
|
{
|
||||||
|
bool isFirstEnchantment = true;
|
||||||
|
foreach (Dictionary<string, object> enchantment in (object[])enchantments)
|
||||||
|
{
|
||||||
|
short level = (short)enchantment["lvl"];
|
||||||
|
string id = ((string)enchantment["id"]).Replace(':', '.');
|
||||||
|
string name = Protocol.Message.ChatParser.TranslateString("enchantment." + id) ?? id;
|
||||||
|
string levelStr = Protocol.Message.ChatParser.TranslateString("enchantment.level." + level) ?? level.ToString();
|
||||||
|
if (isFirstEnchantment)
|
||||||
|
{
|
||||||
|
isFirstEnchantment = false;
|
||||||
|
sb.Append($"{name} {levelStr}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append($" | {name} {levelStr}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PerformAction(int slotId, WindowActionType action)
|
||||||
|
{
|
||||||
|
bool result = Handler.DoWindowAction(WindowId, slotId, action);
|
||||||
|
RefreshFromContainer();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
MinecraftClient/Tui/CraftingView.cs
Normal file
112
MinecraftClient/Tui/CraftingView.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class CraftingView : ContainerViewBase
|
||||||
|
{
|
||||||
|
private readonly CraftingViewModel _craftVm;
|
||||||
|
|
||||||
|
public CraftingView(McClient handler, int windowId)
|
||||||
|
: base(new CraftingViewModel(handler, windowId))
|
||||||
|
{
|
||||||
|
_craftVm = (CraftingViewModel)_vm;
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return 3 + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
var row = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
var gridPanel = new StackPanel { Spacing = 0 };
|
||||||
|
gridPanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_crafting_grid,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
gridPanel.Children.Add(BuildSlotGrid(_craftVm.CraftingGridSlots, 3));
|
||||||
|
row.Children.Add(gridPanel);
|
||||||
|
|
||||||
|
row.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = " \u2192 ",
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
var outPanel = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
outPanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_inventory_output,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
outPanel.Children.Add(CreateSlotCell(_craftVm.OutputSlot, 0, 0));
|
||||||
|
row.Children.Add(outPanel);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CraftingViewModel : ContainerViewModel
|
||||||
|
{
|
||||||
|
public ObservableCollection<SlotViewModel> CraftingGridSlots { get; } = new();
|
||||||
|
public SlotViewModel OutputSlot { get; private set; } = null!;
|
||||||
|
|
||||||
|
public CraftingViewModel(McClient handler, int windowId)
|
||||||
|
: base(handler, windowId, ContainerType.Crafting)
|
||||||
|
{
|
||||||
|
OutputSlot = SlotMap[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeSlots()
|
||||||
|
{
|
||||||
|
SlotMap.Clear();
|
||||||
|
|
||||||
|
var output = new SlotViewModel(0);
|
||||||
|
SlotMap[0] = output;
|
||||||
|
|
||||||
|
for (int i = 1; i <= 9; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
CraftingGridSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 10; i <= 36; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
MainInventorySlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 37; i <= 45; i++)
|
||||||
|
{
|
||||||
|
int hotbarIdx = i - 37;
|
||||||
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
|
HotbarSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
MinecraftClient/Tui/EnchantingTableView.cs
Normal file
198
MinecraftClient/Tui/EnchantingTableView.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class EnchantingTableView : ContainerViewBase
|
||||||
|
{
|
||||||
|
private readonly EnchantingViewModel _enchantVm;
|
||||||
|
private readonly TextBlock[] _enchantNameLabels = new TextBlock[3];
|
||||||
|
private readonly TextBlock[] _enchantCostLabels = new TextBlock[3];
|
||||||
|
|
||||||
|
public EnchantingTableView(McClient handler, int windowId)
|
||||||
|
: base(new EnchantingViewModel(handler, windowId))
|
||||||
|
{
|
||||||
|
_enchantVm = (EnchantingViewModel)_vm;
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshEnchantOptions()
|
||||||
|
{
|
||||||
|
var container = _vm.Handler.GetInventory(_vm.WindowId);
|
||||||
|
if (container == null) return;
|
||||||
|
|
||||||
|
int protocolVersion = _vm.Handler.GetProtocolVersion();
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
if (_enchantNameLabels[i] == null) continue;
|
||||||
|
|
||||||
|
short levelReq = container.Properties.TryGetValue(i, out var lr) ? lr : (short)0;
|
||||||
|
short enchantId = container.Properties.TryGetValue(i + 4, out var eid) ? eid : (short)-1;
|
||||||
|
short enchantLevel = container.Properties.TryGetValue(i + 7, out var el) ? el : (short)0;
|
||||||
|
|
||||||
|
if (levelReq > 0 && enchantId >= 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var enchant = EnchantmentMapping.GetEnchantmentById(protocolVersion, enchantId);
|
||||||
|
string name = EnchantmentMapping.GetEnchantmentName(enchant);
|
||||||
|
string roman = EnchantmentMapping.ConvertLevelToRomanNumbers(enchantLevel);
|
||||||
|
_enchantNameLabels[i].Text = $"{name} {roman}";
|
||||||
|
_enchantCostLabels[i].Text = $" ({levelReq})";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_enchantNameLabels[i].Text = string.Format(Translations.tui_enchanting_option_slot, i + 1);
|
||||||
|
_enchantCostLabels[i].Text = levelReq > 0 ? $" ({levelReq})" : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_enchantNameLabels[i].Text = string.Format(Translations.tui_enchanting_option_slot, i + 1);
|
||||||
|
_enchantCostLabels[i].Text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnContainerDataChanged()
|
||||||
|
{
|
||||||
|
RefreshEnchantOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return 3 + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Spacing = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var slotsCol = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 0,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
slotsCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_enchanting_item,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
slotsCol.Children.Add(CreateSlotCell(_enchantVm.ItemSlot, 0, 0));
|
||||||
|
|
||||||
|
slotsCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_enchanting_lapis,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(60, 80, 200)),
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
slotsCol.Children.Add(CreateSlotCell(_enchantVm.LapisSlot, 1, 0));
|
||||||
|
|
||||||
|
panel.Children.Add(slotsCol);
|
||||||
|
|
||||||
|
var optionsCol = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(2, 0, 0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
optionsCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_enchanting_options,
|
||||||
|
Foreground = Brushes.Magenta,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
});
|
||||||
|
|
||||||
|
int optionWidth = System.Math.Max(_slotW * 4, 30);
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var nameLabel = new TextBlock
|
||||||
|
{
|
||||||
|
Text = string.Format(Translations.tui_enchanting_option_slot, i + 1),
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(100, 200, 70)),
|
||||||
|
TextWrapping = TextWrapping.NoWrap,
|
||||||
|
};
|
||||||
|
_enchantNameLabels[i] = nameLabel;
|
||||||
|
|
||||||
|
var costLabel = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(100, 200, 70)),
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
_enchantCostLabels[i] = costLabel;
|
||||||
|
|
||||||
|
var content = new DockPanel();
|
||||||
|
DockPanel.SetDock(costLabel, Dock.Right);
|
||||||
|
content.Children.Add(costLabel);
|
||||||
|
content.Children.Add(nameLabel);
|
||||||
|
|
||||||
|
optionsCol.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Background = new SolidColorBrush(Color.FromRgb(55, 50, 40)),
|
||||||
|
MinWidth = optionWidth,
|
||||||
|
MinHeight = _slotH,
|
||||||
|
Padding = new Thickness(1, 0),
|
||||||
|
Child = content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshEnchantOptions();
|
||||||
|
|
||||||
|
panel.Children.Add(optionsCol);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnchantingViewModel : ContainerViewModel
|
||||||
|
{
|
||||||
|
public SlotViewModel ItemSlot { get; private set; } = null!;
|
||||||
|
public SlotViewModel LapisSlot { get; private set; } = null!;
|
||||||
|
|
||||||
|
public EnchantingViewModel(McClient handler, int windowId)
|
||||||
|
: base(handler, windowId, ContainerType.Enchantment)
|
||||||
|
{
|
||||||
|
ItemSlot = SlotMap[0];
|
||||||
|
LapisSlot = SlotMap[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeSlots()
|
||||||
|
{
|
||||||
|
SlotMap.Clear();
|
||||||
|
|
||||||
|
SlotMap[0] = new SlotViewModel(0);
|
||||||
|
SlotMap[1] = new SlotViewModel(1);
|
||||||
|
|
||||||
|
for (int i = 2; i <= 28; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
MainInventorySlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 29; i <= 37; i++)
|
||||||
|
{
|
||||||
|
int hotbarIdx = i - 29;
|
||||||
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
|
HotbarSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
MinecraftClient/Tui/FurnaceView.cs
Normal file
133
MinecraftClient/Tui/FurnaceView.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class FurnaceView : ContainerViewBase
|
||||||
|
{
|
||||||
|
private readonly FurnaceViewModel _furnaceVm;
|
||||||
|
|
||||||
|
public FurnaceView(McClient handler, int windowId, ContainerType type)
|
||||||
|
: base(new FurnaceViewModel(handler, windowId, type))
|
||||||
|
{
|
||||||
|
_furnaceVm = (FurnaceViewModel)_vm;
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return 3 + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Spacing = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var leftCol = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 0,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
leftCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_furnace_input,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
leftCol.Children.Add(CreateSlotCell(_furnaceVm.InputSlot, 0, 0));
|
||||||
|
|
||||||
|
leftCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\u2592\u2592\u2592",
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(180, 100, 40)),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
leftCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_furnace_fuel,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
leftCol.Children.Add(CreateSlotCell(_furnaceVm.FuelSlot, 1, 0));
|
||||||
|
|
||||||
|
panel.Children.Add(leftCol);
|
||||||
|
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = " \u2192 ",
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
var rightCol = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
rightCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_furnace_output,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
rightCol.Children.Add(CreateSlotCell(_furnaceVm.OutputSlot, 0, 1));
|
||||||
|
|
||||||
|
panel.Children.Add(rightCol);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FurnaceViewModel : ContainerViewModel
|
||||||
|
{
|
||||||
|
public SlotViewModel InputSlot { get; private set; } = null!;
|
||||||
|
public SlotViewModel FuelSlot { get; private set; } = null!;
|
||||||
|
public SlotViewModel OutputSlot { get; private set; } = null!;
|
||||||
|
|
||||||
|
public FurnaceViewModel(McClient handler, int windowId, ContainerType type)
|
||||||
|
: base(handler, windowId, type)
|
||||||
|
{
|
||||||
|
InputSlot = SlotMap[0];
|
||||||
|
FuelSlot = SlotMap[1];
|
||||||
|
OutputSlot = SlotMap[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeSlots()
|
||||||
|
{
|
||||||
|
SlotMap.Clear();
|
||||||
|
|
||||||
|
SlotMap[0] = new SlotViewModel(0);
|
||||||
|
SlotMap[1] = new SlotViewModel(1);
|
||||||
|
SlotMap[2] = new SlotViewModel(2);
|
||||||
|
|
||||||
|
for (int i = 3; i <= 29; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
MainInventorySlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 30; i <= 38; i++)
|
||||||
|
{
|
||||||
|
int hotbarIdx = i - 30;
|
||||||
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
|
HotbarSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
MinecraftClient/Tui/GridContainerView.cs
Normal file
31
MinecraftClient/Tui/GridContainerView.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class GridContainerView : ContainerViewBase
|
||||||
|
{
|
||||||
|
private readonly int _gridRows;
|
||||||
|
private readonly int _gridCols;
|
||||||
|
|
||||||
|
public GridContainerView(McClient handler, int windowId, ContainerType type, int rows, int cols)
|
||||||
|
: base(new ContainerViewModel(handler, windowId, type))
|
||||||
|
{
|
||||||
|
_gridRows = rows;
|
||||||
|
_gridCols = cols;
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return _gridRows + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
return BuildSlotGrid(_vm.ContainerSlots, _gridCols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
MinecraftClient/Tui/GrindstoneView.cs
Normal file
126
MinecraftClient/Tui/GrindstoneView.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class GrindstoneView : ContainerViewBase
|
||||||
|
{
|
||||||
|
private readonly GrindstoneViewModel _grindVm;
|
||||||
|
|
||||||
|
public GrindstoneView(McClient handler, int windowId)
|
||||||
|
: base(new GrindstoneViewModel(handler, windowId))
|
||||||
|
{
|
||||||
|
_grindVm = (GrindstoneViewModel)_vm;
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return 2 + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
var row = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Spacing = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var inputCol = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 0,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
inputCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_grindstone_input1,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
inputCol.Children.Add(CreateSlotCell(_grindVm.Input1Slot, 0, 0));
|
||||||
|
|
||||||
|
inputCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_grindstone_input2,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
inputCol.Children.Add(CreateSlotCell(_grindVm.Input2Slot, 1, 0));
|
||||||
|
|
||||||
|
row.Children.Add(inputCol);
|
||||||
|
|
||||||
|
row.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "=>",
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Padding = new Thickness(1, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
var outCol = new StackPanel
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
outCol.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = Translations.tui_inventory_output,
|
||||||
|
Foreground = BrEquipLbl,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
});
|
||||||
|
outCol.Children.Add(CreateSlotCell(_grindVm.OutputSlot, 0, 1));
|
||||||
|
row.Children.Add(outCol);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GrindstoneViewModel : ContainerViewModel
|
||||||
|
{
|
||||||
|
public SlotViewModel Input1Slot { get; private set; } = null!;
|
||||||
|
public SlotViewModel Input2Slot { get; private set; } = null!;
|
||||||
|
public SlotViewModel OutputSlot { get; private set; } = null!;
|
||||||
|
|
||||||
|
public GrindstoneViewModel(McClient handler, int windowId)
|
||||||
|
: base(handler, windowId, ContainerType.Grindstone)
|
||||||
|
{
|
||||||
|
Input1Slot = SlotMap[0];
|
||||||
|
Input2Slot = SlotMap[1];
|
||||||
|
OutputSlot = SlotMap[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeSlots()
|
||||||
|
{
|
||||||
|
SlotMap.Clear();
|
||||||
|
|
||||||
|
SlotMap[0] = new SlotViewModel(0);
|
||||||
|
SlotMap[1] = new SlotViewModel(1);
|
||||||
|
SlotMap[2] = new SlotViewModel(2);
|
||||||
|
|
||||||
|
for (int i = 3; i <= 29; i++)
|
||||||
|
{
|
||||||
|
var slot = new SlotViewModel(i);
|
||||||
|
MainInventorySlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 30; i <= 38; i++)
|
||||||
|
{
|
||||||
|
int hotbarIdx = i - 30;
|
||||||
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
|
HotbarSlots.Add(slot);
|
||||||
|
SlotMap[i] = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
MinecraftClient/Tui/HopperView.cs
Normal file
30
MinecraftClient/Tui/HopperView.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public class HopperView : ContainerViewBase
|
||||||
|
{
|
||||||
|
public HopperView(McClient handler, int windowId)
|
||||||
|
: base(new ContainerViewModel(handler, windowId, ContainerType.Hopper))
|
||||||
|
{
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetTotalSlotRows()
|
||||||
|
{
|
||||||
|
return 1 + 3 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control BuildContainerSpecificArea()
|
||||||
|
{
|
||||||
|
var grid = BuildSlotGrid(_vm.ContainerSlots, 5);
|
||||||
|
return new StackPanel
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Children = { grid },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Consolonia.Themes;
|
using Consolonia.Themes;
|
||||||
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
namespace MinecraftClient.Tui
|
namespace MinecraftClient.Tui
|
||||||
{
|
{
|
||||||
|
|
@ -16,9 +17,15 @@ namespace MinecraftClient.Tui
|
||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
|
var handler = InventoryTuiHost.ActiveHandler!;
|
||||||
|
var windowId = InventoryTuiHost.ActiveWindowId;
|
||||||
|
var container = handler.GetInventory(windowId);
|
||||||
|
var containerType = container?.Type ?? ContainerType.PlayerInventory;
|
||||||
|
var view = ContainerViewBase.CreateView(containerType, handler, windowId);
|
||||||
|
|
||||||
desktop.MainWindow = new Window
|
desktop.MainWindow = new Window
|
||||||
{
|
{
|
||||||
Content = new InventoryMainView(),
|
Content = view,
|
||||||
Title = "MCC Inventory"
|
Title = "MCC Inventory"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,304 +1,39 @@
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.Controls.Primitives;
|
|
||||||
using Avalonia.Controls.Templates;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using MinecraftClient.Inventory;
|
|
||||||
|
|
||||||
namespace MinecraftClient.Tui
|
namespace MinecraftClient.Tui
|
||||||
{
|
{
|
||||||
public class InventoryMainView : UserControl
|
public class PlayerInventoryView : ContainerViewBase
|
||||||
{
|
{
|
||||||
private static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40));
|
private readonly PlayerInventoryViewModel _playerVm;
|
||||||
private static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55));
|
|
||||||
private static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75));
|
|
||||||
private static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90));
|
|
||||||
private static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140));
|
|
||||||
private static readonly IBrush BrName = Brushes.White;
|
|
||||||
private static readonly IBrush BrCount = Brushes.Yellow;
|
|
||||||
private static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80));
|
|
||||||
private static readonly IBrush BrEquipLbl = Brushes.DarkCyan;
|
|
||||||
private static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60));
|
|
||||||
private static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80));
|
|
||||||
private static readonly IBrush BrHeldItemBorder = Brushes.Yellow;
|
|
||||||
|
|
||||||
private int _slotW;
|
|
||||||
private int _slotH;
|
|
||||||
private int _nameMaxLen;
|
|
||||||
private int _nameLines;
|
|
||||||
private int _topGap;
|
private int _topGap;
|
||||||
private int _termW;
|
|
||||||
|
|
||||||
private readonly InventoryViewModel _vm;
|
public PlayerInventoryView(McClient handler, int windowId)
|
||||||
private TextBlock _titleText = null!;
|
: base(new PlayerInventoryViewModel(handler, windowId))
|
||||||
private Border _infoDetailBorder = null!;
|
|
||||||
private TextBlock _infoDetailText = null!;
|
|
||||||
private TextBlock _cursorItemText = null!;
|
|
||||||
private TextBlock _helpText = null!;
|
|
||||||
|
|
||||||
private TextBlock[] _hotbarIndicators = new TextBlock[9];
|
|
||||||
private int _currentHotbarSlot = -1;
|
|
||||||
|
|
||||||
private Border? _lastHoveredSlotBorder;
|
|
||||||
|
|
||||||
private Canvas _overlayCanvas = null!;
|
|
||||||
private Border _heldItemFloater = null!;
|
|
||||||
private TextBlock _heldItemFloaterName = null!;
|
|
||||||
private TextBlock _heldItemFloaterCount = null!;
|
|
||||||
|
|
||||||
private ScrollViewer _chatScrollViewer = null!;
|
|
||||||
private ObservableCollection<string>? _chatLines;
|
|
||||||
private int _lastTermW;
|
|
||||||
private int _lastTermH;
|
|
||||||
|
|
||||||
public InventoryMainView()
|
|
||||||
{
|
{
|
||||||
var handler = InventoryTuiHost.ActiveHandler
|
_playerVm = (PlayerInventoryViewModel)_vm;
|
||||||
?? throw new InvalidOperationException("No active McClient");
|
Initialize();
|
||||||
int windowId = InventoryTuiHost.ActiveWindowId;
|
|
||||||
|
|
||||||
_vm = new InventoryViewModel(handler, windowId);
|
|
||||||
_currentHotbarSlot = handler.GetCurrentSlot();
|
|
||||||
|
|
||||||
_chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50)
|
|
||||||
?? new ObservableCollection<string>();
|
|
||||||
|
|
||||||
RebuildUi();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RebuildUi()
|
protected override int GetTotalSlotRows()
|
||||||
{
|
{
|
||||||
int termH;
|
return 6;
|
||||||
try
|
|
||||||
{
|
|
||||||
_termW = System.Console.WindowWidth;
|
|
||||||
termH = System.Console.WindowHeight;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_termW = 120;
|
|
||||||
termH = 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastTermW = _termW;
|
|
||||||
_lastTermH = termH;
|
|
||||||
|
|
||||||
int availW = _termW - 26;
|
|
||||||
_slotW = Math.Clamp(availW / 9, 8, 18);
|
|
||||||
_nameMaxLen = _slotW;
|
|
||||||
|
|
||||||
int topUsedW = _slotW * 4 + 8 + _slotW * 2 + 4 + _slotW;
|
|
||||||
_topGap = Math.Max(2, (_slotW * 9 - topUsedW) / 2);
|
|
||||||
|
|
||||||
_slotH = Math.Clamp((termH - 8) / 6, 2, 5);
|
|
||||||
_nameLines = _slotH;
|
|
||||||
|
|
||||||
_vm.SetSlotDisplayParams(_nameMaxLen, _nameLines);
|
|
||||||
|
|
||||||
_lastHoveredSlotBorder = null;
|
|
||||||
|
|
||||||
_titleText = new TextBlock
|
|
||||||
{
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
Foreground = Brushes.Cyan,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
};
|
|
||||||
|
|
||||||
_infoDetailText = new TextBlock
|
|
||||||
{
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Foreground = Brushes.White,
|
|
||||||
};
|
|
||||||
|
|
||||||
_infoDetailBorder = new Border
|
|
||||||
{
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
Child = _infoDetailText,
|
|
||||||
};
|
|
||||||
|
|
||||||
_cursorItemText = new TextBlock
|
|
||||||
{
|
|
||||||
Foreground = Brushes.Yellow,
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
};
|
|
||||||
|
|
||||||
_helpText = new TextBlock
|
|
||||||
{
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
|
||||||
Text = Translations.tui_inventory_controls_help,
|
|
||||||
};
|
|
||||||
|
|
||||||
_heldItemFloaterName = new TextBlock
|
|
||||||
{
|
|
||||||
Foreground = Brushes.White,
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
};
|
|
||||||
_heldItemFloaterCount = new TextBlock
|
|
||||||
{
|
|
||||||
Foreground = BrCount,
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
};
|
|
||||||
_heldItemFloater = new Border
|
|
||||||
{
|
|
||||||
Background = BrHeldItemBg,
|
|
||||||
BorderBrush = BrHeldItemBorder,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
Padding = new Thickness(1, 0),
|
|
||||||
IsVisible = false,
|
|
||||||
MaxWidth = 24,
|
|
||||||
Child = new StackPanel
|
|
||||||
{
|
|
||||||
Children = { _heldItemFloaterName, _heldItemFloaterCount },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
_overlayCanvas = new Canvas { IsHitTestVisible = false };
|
|
||||||
_overlayCanvas.Children.Add(_heldItemFloater);
|
|
||||||
|
|
||||||
var chatLines = _chatLines!;
|
|
||||||
chatLines.CollectionChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
var sv = _chatScrollViewer;
|
|
||||||
if (sv.Extent.Height > sv.Viewport.Height)
|
|
||||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
|
||||||
}, Avalonia.Threading.DispatcherPriority.Background);
|
|
||||||
};
|
|
||||||
var chatItemsControl = new ItemsControl
|
|
||||||
{
|
|
||||||
ItemsSource = chatLines,
|
|
||||||
Focusable = false,
|
|
||||||
ItemTemplate = new FuncDataTemplate<string>((s, _) =>
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = s,
|
|
||||||
Foreground = Brushes.Gray,
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
Margin = new Thickness(0),
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
_chatScrollViewer = new ScrollViewer
|
|
||||||
{
|
|
||||||
Content = chatItemsControl,
|
|
||||||
Background = Brushes.Black,
|
|
||||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
|
||||||
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
_hotbarIndicators = new TextBlock[9];
|
|
||||||
|
|
||||||
Content = BuildRootLayout();
|
|
||||||
UpdateTitle();
|
|
||||||
UpdateInfoPanel();
|
|
||||||
|
|
||||||
_chatScrollToBottom = true;
|
|
||||||
_chatScrollViewer.ScrollChanged += OnChatScrollChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _chatScrollToBottom = true;
|
protected override void RebuildUi()
|
||||||
|
|
||||||
private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e)
|
|
||||||
{
|
{
|
||||||
if (!_chatScrollToBottom) return;
|
int availW = 0;
|
||||||
var sv = _chatScrollViewer;
|
try { availW = System.Console.WindowWidth - 26; } catch { availW = 94; }
|
||||||
if (sv.Extent.Height > sv.Viewport.Height)
|
int slotW = System.Math.Clamp(availW / 9, 8, 18);
|
||||||
{
|
int topUsedW = slotW * 4 + 8 + slotW * 2 + 4 + slotW;
|
||||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
_topGap = System.Math.Max(2, (slotW * 9 - topUsedW) / 2);
|
||||||
_chatScrollToBottom = false;
|
|
||||||
}
|
base.RebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Control BuildRootLayout()
|
protected override Control BuildContainerSpecificArea()
|
||||||
{
|
|
||||||
// Layout (top-down):
|
|
||||||
// Title
|
|
||||||
// [InfoPanel(right)] [InventoryGrid(left)] <-- inventory area
|
|
||||||
// ChatScrollViewer (full width, fills remaining)
|
|
||||||
|
|
||||||
var inventoryArea = BuildMainArea();
|
|
||||||
DockPanel.SetDock(_titleText, Dock.Top);
|
|
||||||
DockPanel.SetDock(inventoryArea, Dock.Top);
|
|
||||||
|
|
||||||
var mainContent = new DockPanel
|
|
||||||
{
|
|
||||||
Children = { _titleText, inventoryArea, _chatScrollViewer }
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Panel
|
|
||||||
{
|
|
||||||
Background = Brushes.Black,
|
|
||||||
Children = { mainContent, _overlayCanvas }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Control BuildMainArea()
|
|
||||||
{
|
|
||||||
var infoPanel = BuildInfoPanel();
|
|
||||||
DockPanel.SetDock(infoPanel, Dock.Right);
|
|
||||||
|
|
||||||
return new DockPanel
|
|
||||||
{
|
|
||||||
Children = { infoPanel, BuildInventoryPanel() }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Control BuildInfoPanel()
|
|
||||||
{
|
|
||||||
return new Border
|
|
||||||
{
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
BorderBrush = Brushes.Gray,
|
|
||||||
Padding = new Thickness(1),
|
|
||||||
Width = 24,
|
|
||||||
Child = new StackPanel
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan },
|
|
||||||
_infoDetailBorder,
|
|
||||||
new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) },
|
|
||||||
_cursorItemText,
|
|
||||||
new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) },
|
|
||||||
_helpText,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Control BuildInventoryPanel()
|
|
||||||
{
|
|
||||||
var root = new StackPanel
|
|
||||||
{
|
|
||||||
Spacing = 0,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
};
|
|
||||||
|
|
||||||
root.Children.Add(BuildTopSection());
|
|
||||||
root.Children.Add(new Border { Height = 1 });
|
|
||||||
root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9));
|
|
||||||
root.Children.Add(BuildHotbarSection());
|
|
||||||
|
|
||||||
return new Border
|
|
||||||
{
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
BorderBrush = Brushes.Gray,
|
|
||||||
Child = root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Control BuildTopSection()
|
|
||||||
{
|
{
|
||||||
var row = new StackPanel
|
var row = new StackPanel
|
||||||
{
|
{
|
||||||
|
|
@ -318,7 +53,7 @@ namespace MinecraftClient.Tui
|
||||||
FontWeight = FontWeight.Bold,
|
FontWeight = FontWeight.Bold,
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
});
|
});
|
||||||
offPanel.Children.Add(CreateSlotCell(_vm.OffhandSlot, 0, 0));
|
offPanel.Children.Add(CreateSlotCell(_playerVm.OffhandSlot, 0, 0));
|
||||||
row.Children.Add(offPanel);
|
row.Children.Add(offPanel);
|
||||||
|
|
||||||
var equipGrid = new Grid
|
var equipGrid = new Grid
|
||||||
|
|
@ -332,7 +67,7 @@ namespace MinecraftClient.Tui
|
||||||
var lbl = MakeLabel(label);
|
var lbl = MakeLabel(label);
|
||||||
Grid.SetRow(lbl, r); Grid.SetColumn(lbl, gc);
|
Grid.SetRow(lbl, r); Grid.SetColumn(lbl, gc);
|
||||||
equipGrid.Children.Add(lbl);
|
equipGrid.Children.Add(lbl);
|
||||||
var btn = CreateSlotCell(_vm.EquipmentSlots[eqIdx], r, gc / 2);
|
var btn = CreateSlotCell(_playerVm.EquipmentSlots[eqIdx], r, gc / 2);
|
||||||
Grid.SetRow(btn, r); Grid.SetColumn(btn, gc + 1);
|
Grid.SetRow(btn, r); Grid.SetColumn(btn, gc + 1);
|
||||||
equipGrid.Children.Add(btn);
|
equipGrid.Children.Add(btn);
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +89,7 @@ namespace MinecraftClient.Tui
|
||||||
for (int ci = 0; ci < 4; ci++)
|
for (int ci = 0; ci < 4; ci++)
|
||||||
{
|
{
|
||||||
int cr = ci / 2, cc = ci % 2;
|
int cr = ci / 2, cc = ci % 2;
|
||||||
var cs = CreateSlotCell(_vm.CraftingInputSlots[ci], cr, cc);
|
var cs = CreateSlotCell(_playerVm.CraftingInputSlots[ci], cr, cc);
|
||||||
Grid.SetRow(cs, cr);
|
Grid.SetRow(cs, cr);
|
||||||
Grid.SetColumn(cs, cc);
|
Grid.SetColumn(cs, cc);
|
||||||
craftGrid.Children.Add(cs);
|
craftGrid.Children.Add(cs);
|
||||||
|
|
@ -382,7 +117,7 @@ namespace MinecraftClient.Tui
|
||||||
FontWeight = FontWeight.Bold,
|
FontWeight = FontWeight.Bold,
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
});
|
});
|
||||||
craftOutPanel.Children.Add(CreateSlotCell(_vm.CraftingOutputSlot, 0, 1));
|
craftOutPanel.Children.Add(CreateSlotCell(_playerVm.CraftingOutputSlot, 0, 1));
|
||||||
Grid.SetRow(craftOutPanel, 0); Grid.SetColumn(craftOutPanel, 3);
|
Grid.SetRow(craftOutPanel, 0); Grid.SetColumn(craftOutPanel, 3);
|
||||||
Grid.SetRowSpan(craftOutPanel, 2);
|
Grid.SetRowSpan(craftOutPanel, 2);
|
||||||
craftGrid.Children.Add(craftOutPanel);
|
craftGrid.Children.Add(craftOutPanel);
|
||||||
|
|
@ -390,363 +125,5 @@ namespace MinecraftClient.Tui
|
||||||
row.Children.Add(craftGrid);
|
row.Children.Add(craftGrid);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Control BuildHotbarSection()
|
|
||||||
{
|
|
||||||
var panel = new StackPanel { Spacing = 0 };
|
|
||||||
|
|
||||||
var numberRow = new StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Orientation.Horizontal,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
};
|
|
||||||
for (int i = 0; i < 9; i++)
|
|
||||||
{
|
|
||||||
bool active = i == _currentHotbarSlot;
|
|
||||||
string label = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
|
||||||
|
|
||||||
var tb = new TextBlock
|
|
||||||
{
|
|
||||||
Text = label,
|
|
||||||
Width = _slotW,
|
|
||||||
TextAlignment = TextAlignment.Center,
|
|
||||||
Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan,
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
};
|
|
||||||
_hotbarIndicators[i] = tb;
|
|
||||||
numberRow.Children.Add(tb);
|
|
||||||
}
|
|
||||||
panel.Children.Add(numberRow);
|
|
||||||
panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9));
|
|
||||||
return panel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextBlock MakeLabel(string text)
|
|
||||||
{
|
|
||||||
return new TextBlock
|
|
||||||
{
|
|
||||||
Text = text,
|
|
||||||
Foreground = BrEquipLbl,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = new Thickness(1, 0, 0, 0),
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Control BuildSlotGrid(ObservableCollection<SlotViewModel> slots, int columns)
|
|
||||||
{
|
|
||||||
var grid = new Grid();
|
|
||||||
int rows = (slots.Count + columns - 1) / columns;
|
|
||||||
|
|
||||||
for (int r = 0; r < rows; r++)
|
|
||||||
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
|
||||||
for (int c = 0; c < columns; c++)
|
|
||||||
grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
|
||||||
|
|
||||||
for (int i = 0; i < slots.Count; i++)
|
|
||||||
{
|
|
||||||
int row = i / columns;
|
|
||||||
int col = i % columns;
|
|
||||||
var cell = CreateSlotCell(slots[i], row, col);
|
|
||||||
Grid.SetRow(cell, row);
|
|
||||||
Grid.SetColumn(cell, col);
|
|
||||||
grid.Children.Add(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IBrush GetSlotBg(bool isEmpty, int row, int col)
|
|
||||||
{
|
|
||||||
bool isA = (row + col) % 2 == 0;
|
|
||||||
return isEmpty
|
|
||||||
? (isA ? BrSlotEmptyA : BrSlotEmptyB)
|
|
||||||
: (isA ? BrSlotFillA : BrSlotFillB);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0)
|
|
||||||
{
|
|
||||||
var nameTb = new TextBlock
|
|
||||||
{
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
Margin = new Thickness(0),
|
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
|
||||||
};
|
|
||||||
|
|
||||||
var countTb = new TextBlock
|
|
||||||
{
|
|
||||||
Foreground = BrCount,
|
|
||||||
FontWeight = FontWeight.Bold,
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
Margin = new Thickness(0),
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
|
||||||
VerticalAlignment = VerticalAlignment.Bottom,
|
|
||||||
};
|
|
||||||
|
|
||||||
ApplySlotVisual(slot, nameTb, countTb);
|
|
||||||
|
|
||||||
int r = row, c = col;
|
|
||||||
var border = new Border
|
|
||||||
{
|
|
||||||
Width = _slotW,
|
|
||||||
Height = _slotH,
|
|
||||||
Background = GetSlotBg(slot.IsEmpty, r, c),
|
|
||||||
Child = new Panel
|
|
||||||
{
|
|
||||||
Children = { nameTb, countTb },
|
|
||||||
},
|
|
||||||
Tag = (slot, r, c),
|
|
||||||
};
|
|
||||||
|
|
||||||
border.PointerPressed += OnSlotPointerPressed;
|
|
||||||
border.PointerEntered += OnSlotPointerEnter;
|
|
||||||
border.PointerExited += OnSlotPointerExit;
|
|
||||||
border.PointerMoved += OnSlotPointerMoved;
|
|
||||||
|
|
||||||
slot.PropertyChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
ApplySlotVisual(slot, nameTb, countTb);
|
|
||||||
border.Background = GetSlotBg(slot.IsEmpty, r, c);
|
|
||||||
};
|
|
||||||
|
|
||||||
return border;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb)
|
|
||||||
{
|
|
||||||
if (slot.IsEmpty)
|
|
||||||
{
|
|
||||||
nameTb.Text = "";
|
|
||||||
nameTb.Foreground = BrDim;
|
|
||||||
countTb.Text = "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nameTb.Text = slot.ItemDisplayText;
|
|
||||||
nameTb.Foreground = BrName;
|
|
||||||
countTb.Text = slot.CountDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int))
|
|
||||||
return;
|
|
||||||
|
|
||||||
SetHover(border, slot);
|
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(border);
|
|
||||||
bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0;
|
|
||||||
|
|
||||||
WindowActionType action;
|
|
||||||
if (point.Properties.IsRightButtonPressed)
|
|
||||||
action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick;
|
|
||||||
else
|
|
||||||
action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick;
|
|
||||||
|
|
||||||
_vm.PerformAction(slot.SlotId, action);
|
|
||||||
UpdateInfoPanel();
|
|
||||||
UpdateHeldItemFloater(e);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSlotPointerEnter(object? sender, PointerEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
|
||||||
{
|
|
||||||
SetHover(b, slot);
|
|
||||||
UpdateHeldItemFloater(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSlotPointerMoved(object? sender, PointerEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
|
||||||
{
|
|
||||||
SetHover(b, slot);
|
|
||||||
UpdateHeldItemFloater(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSlotPointerExit(object? sender, PointerEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col))
|
|
||||||
b.Background = GetSlotBg(slot.IsEmpty, row, col);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetHover(Border border, SlotViewModel slot)
|
|
||||||
{
|
|
||||||
if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border)
|
|
||||||
{
|
|
||||||
if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc))
|
|
||||||
_lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc);
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastHoveredSlotBorder = border;
|
|
||||||
border.Background = BrSlotHover;
|
|
||||||
_vm.HoveredSlot = slot;
|
|
||||||
UpdateInfoPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateHeldItemFloater(PointerEventArgs e)
|
|
||||||
{
|
|
||||||
if (!_vm.HasCursorItem)
|
|
||||||
{
|
|
||||||
_heldItemFloater.IsVisible = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_heldItemFloaterName.Text = _vm.CursorItemInfo;
|
|
||||||
_heldItemFloaterCount.Text = "";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var pos = e.GetPosition(_overlayCanvas);
|
|
||||||
double left = pos.X + 2;
|
|
||||||
double remainingW = _termW - left - 2;
|
|
||||||
int maxW = Math.Max(8, (int)remainingW);
|
|
||||||
_heldItemFloater.MaxWidth = maxW;
|
|
||||||
Canvas.SetLeft(_heldItemFloater, left);
|
|
||||||
Canvas.SetTop(_heldItemFloater, pos.Y);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_heldItemFloater.MaxWidth = 24;
|
|
||||||
Canvas.SetLeft(_heldItemFloater, 0);
|
|
||||||
Canvas.SetTop(_heldItemFloater, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
_heldItemFloater.IsVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateInfoPanel()
|
|
||||||
{
|
|
||||||
_infoDetailText.Text = _vm.HoveredSlotDetailText;
|
|
||||||
|
|
||||||
bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty;
|
|
||||||
_infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent;
|
|
||||||
|
|
||||||
if (_vm.HasCursorItem)
|
|
||||||
{
|
|
||||||
_cursorItemText.Text = _vm.CursorItemInfo;
|
|
||||||
_cursorItemText.Foreground = Brushes.Yellow;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_cursorItemText.Text = Translations.tui_inventory_cursor_empty;
|
|
||||||
_cursorItemText.Foreground = BrDim;
|
|
||||||
_heldItemFloater.IsVisible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateTitle()
|
|
||||||
{
|
|
||||||
_titleText.Text = _vm.Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseInventory()
|
|
||||||
{
|
|
||||||
if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend)
|
|
||||||
tuiBackend.GetView()?.HideOverlay();
|
|
||||||
else
|
|
||||||
(Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnKeyDown(KeyEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnKeyDown(e);
|
|
||||||
|
|
||||||
switch (e.Key)
|
|
||||||
{
|
|
||||||
case Key.Escape:
|
|
||||||
case Key.E:
|
|
||||||
CloseInventory();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.C:
|
|
||||||
if ((e.KeyModifiers & KeyModifiers.Shift) != 0 &&
|
|
||||||
_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
|
||||||
{
|
|
||||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick);
|
|
||||||
UpdateInfoPanel();
|
|
||||||
}
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.Q:
|
|
||||||
if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
|
||||||
{
|
|
||||||
var action = (e.KeyModifiers & KeyModifiers.Control) != 0
|
|
||||||
? WindowActionType.DropItemStack
|
|
||||||
: WindowActionType.DropItem;
|
|
||||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, action);
|
|
||||||
UpdateInfoPanel();
|
|
||||||
}
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.R:
|
|
||||||
_vm.RefreshFromContainer();
|
|
||||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
|
||||||
UpdateHotbarIndicators();
|
|
||||||
UpdateInfoPanel();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateHotbarIndicators()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 9; i++)
|
|
||||||
{
|
|
||||||
bool active = i == _currentHotbarSlot;
|
|
||||||
_hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
|
||||||
_hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnAttachedToVisualTree(e);
|
|
||||||
Focusable = true;
|
|
||||||
Focus();
|
|
||||||
AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
|
||||||
SizeChanged += OnViewSizeChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Key == Key.Escape)
|
|
||||||
{
|
|
||||||
CloseInventory();
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e)
|
|
||||||
{
|
|
||||||
int newW, newH;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
newW = System.Console.WindowWidth;
|
|
||||||
newH = System.Console.WindowHeight;
|
|
||||||
}
|
|
||||||
catch { return; }
|
|
||||||
|
|
||||||
if (newW == _lastTermW && newH == _lastTermH) return;
|
|
||||||
|
|
||||||
_vm.RefreshFromContainer();
|
|
||||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
|
||||||
RebuildUi();
|
|
||||||
Focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnGotFocus(GotFocusEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnGotFocus(e);
|
|
||||||
Focusable = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,30 @@ namespace MinecraftClient.Tui
|
||||||
|
|
||||||
public static bool IsRunning => _isRunning;
|
public static bool IsRunning => _isRunning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by McClient.OnInventoryClose when the server closes a container.
|
||||||
|
/// If the closed window matches the active TUI window, auto-close the TUI.
|
||||||
|
/// </summary>
|
||||||
|
public static void NotifyInventoryClosed(int windowId)
|
||||||
|
{
|
||||||
|
if (!_isRunning || windowId != ActiveWindowId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ConsoleIO.Backend is TuiConsoleBackend)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var view = TuiConsoleBackend.Instance?.GetView();
|
||||||
|
view?.HideOverlay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(Avalonia.Application.Current?.ApplicationLifetime
|
||||||
|
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime)?.Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the TUI can be launched (classic mode has a one-shot limit).
|
/// Whether the TUI can be launched (classic mode has a one-shot limit).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -89,7 +113,10 @@ namespace MinecraftClient.Tui
|
||||||
var view = TuiConsoleBackend.Instance?.GetView();
|
var view = TuiConsoleBackend.Instance?.GetView();
|
||||||
if (view != null)
|
if (view != null)
|
||||||
{
|
{
|
||||||
var content = new InventoryMainView();
|
var container = ActiveHandler!.GetInventory(ActiveWindowId);
|
||||||
|
var content = ContainerViewBase.CreateView(
|
||||||
|
container?.Type ?? ContainerType.PlayerInventory,
|
||||||
|
ActiveHandler, ActiveWindowId);
|
||||||
view.ShowOverlay(content, () =>
|
view.ShowOverlay(content, () =>
|
||||||
{
|
{
|
||||||
ActiveHandler = null;
|
ActiveHandler = null;
|
||||||
|
|
|
||||||
|
|
@ -1,152 +1,48 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
|
||||||
using MinecraftClient.Inventory;
|
using MinecraftClient.Inventory;
|
||||||
|
|
||||||
namespace MinecraftClient.Tui
|
namespace MinecraftClient.Tui
|
||||||
{
|
{
|
||||||
public class InventoryViewModel : INotifyPropertyChanged
|
public class PlayerInventoryViewModel : ContainerViewModel
|
||||||
{
|
{
|
||||||
private SlotViewModel? _hoveredSlot;
|
|
||||||
private string _title = "";
|
|
||||||
private string _statusText = "";
|
|
||||||
private string _cursorItemInfo = "";
|
|
||||||
private bool _hasCursorItem;
|
|
||||||
|
|
||||||
public McClient Handler { get; }
|
|
||||||
public int WindowId { get; }
|
|
||||||
|
|
||||||
public ObservableCollection<SlotViewModel> EquipmentSlots { get; } = new();
|
public ObservableCollection<SlotViewModel> EquipmentSlots { get; } = new();
|
||||||
public ObservableCollection<SlotViewModel> CraftingInputSlots { get; } = new();
|
public ObservableCollection<SlotViewModel> CraftingInputSlots { get; } = new();
|
||||||
public SlotViewModel CraftingOutputSlot { get; }
|
public SlotViewModel CraftingOutputSlot { get; }
|
||||||
public ObservableCollection<SlotViewModel> MainInventorySlots { get; } = new();
|
|
||||||
public ObservableCollection<SlotViewModel> HotbarSlots { get; } = new();
|
|
||||||
public SlotViewModel OffhandSlot { get; }
|
public SlotViewModel OffhandSlot { get; }
|
||||||
|
|
||||||
public string Title
|
public PlayerInventoryViewModel(McClient handler, int windowId)
|
||||||
|
: base(handler, windowId, ContainerType.PlayerInventory)
|
||||||
{
|
{
|
||||||
get => _title;
|
CraftingOutputSlot = SlotMap[0];
|
||||||
set { _title = value; OnPropertyChanged(); }
|
OffhandSlot = SlotMap[45];
|
||||||
}
|
}
|
||||||
|
|
||||||
public string StatusText
|
protected override void InitializeSlots()
|
||||||
{
|
{
|
||||||
get => _statusText;
|
SlotMap.Clear();
|
||||||
set { _statusText = value; OnPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CursorItemInfo
|
var craftOut = new SlotViewModel(0);
|
||||||
{
|
SlotMap[0] = craftOut;
|
||||||
get => _cursorItemInfo;
|
|
||||||
set { _cursorItemInfo = value; OnPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasCursorItem
|
|
||||||
{
|
|
||||||
get => _hasCursorItem;
|
|
||||||
set { _hasCursorItem = value; OnPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public SlotViewModel? HoveredSlot
|
|
||||||
{
|
|
||||||
get => _hoveredSlot;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_hoveredSlot != null)
|
|
||||||
_hoveredSlot.IsHovered = false;
|
|
||||||
_hoveredSlot = value;
|
|
||||||
if (_hoveredSlot != null)
|
|
||||||
_hoveredSlot.IsHovered = true;
|
|
||||||
OnPropertyChanged();
|
|
||||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multi-line detail text for the hovered slot.
|
|
||||||
/// </summary>
|
|
||||||
public string HoveredSlotDetailText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_hoveredSlot == null)
|
|
||||||
return Translations.tui_inventory_hover_hint;
|
|
||||||
|
|
||||||
if (_hoveredSlot.IsEmpty)
|
|
||||||
return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}";
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine(_hoveredSlot.ItemTypeName);
|
|
||||||
sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount));
|
|
||||||
|
|
||||||
string fullInfo = _hoveredSlot.FullInfo;
|
|
||||||
if (!string.IsNullOrEmpty(fullInfo))
|
|
||||||
{
|
|
||||||
string[] parts = fullInfo.Split(" | ");
|
|
||||||
for (int i = 1; i < parts.Length; i++)
|
|
||||||
sb.AppendLine(parts[i].Trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString().TrimEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<int, SlotViewModel> _slotMap = new();
|
|
||||||
private int _nameMaxLen = 9;
|
|
||||||
private int _nameMaxLines = 1;
|
|
||||||
|
|
||||||
public InventoryViewModel(McClient handler, int windowId)
|
|
||||||
{
|
|
||||||
Handler = handler;
|
|
||||||
WindowId = windowId;
|
|
||||||
|
|
||||||
CraftingOutputSlot = new SlotViewModel(0);
|
|
||||||
OffhandSlot = new SlotViewModel(45);
|
|
||||||
|
|
||||||
InitializeSlots();
|
|
||||||
RefreshFromContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetSlotDisplayParams(int maxWidth, int maxLines)
|
|
||||||
{
|
|
||||||
_nameMaxLen = maxWidth;
|
|
||||||
_nameMaxLines = maxLines;
|
|
||||||
foreach (var kvp in _slotMap)
|
|
||||||
{
|
|
||||||
kvp.Value.NameMaxWidth = maxWidth;
|
|
||||||
kvp.Value.NameMaxLines = maxLines;
|
|
||||||
}
|
|
||||||
RefreshFromContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeSlots()
|
|
||||||
{
|
|
||||||
_slotMap.Clear();
|
|
||||||
|
|
||||||
_slotMap[0] = CraftingOutputSlot;
|
|
||||||
|
|
||||||
for (int i = 1; i <= 4; i++)
|
for (int i = 1; i <= 4; i++)
|
||||||
{
|
{
|
||||||
var slot = new SlotViewModel(i);
|
var slot = new SlotViewModel(i);
|
||||||
CraftingInputSlots.Add(slot);
|
CraftingInputSlots.Add(slot);
|
||||||
_slotMap[i] = slot;
|
SlotMap[i] = slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 5; i <= 8; i++)
|
for (int i = 5; i <= 8; i++)
|
||||||
{
|
{
|
||||||
var slot = new SlotViewModel(i);
|
var slot = new SlotViewModel(i);
|
||||||
EquipmentSlots.Add(slot);
|
EquipmentSlots.Add(slot);
|
||||||
_slotMap[i] = slot;
|
SlotMap[i] = slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 9; i <= 35; i++)
|
for (int i = 9; i <= 35; i++)
|
||||||
{
|
{
|
||||||
var slot = new SlotViewModel(i);
|
var slot = new SlotViewModel(i);
|
||||||
MainInventorySlots.Add(slot);
|
MainInventorySlots.Add(slot);
|
||||||
_slotMap[i] = slot;
|
SlotMap[i] = slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 36; i <= 44; i++)
|
for (int i = 36; i <= 44; i++)
|
||||||
|
|
@ -154,67 +50,11 @@ namespace MinecraftClient.Tui
|
||||||
int hotbarIdx = i - 36;
|
int hotbarIdx = i - 36;
|
||||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||||
HotbarSlots.Add(slot);
|
HotbarSlots.Add(slot);
|
||||||
_slotMap[i] = slot;
|
SlotMap[i] = slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
_slotMap[45] = OffhandSlot;
|
var offhand = new SlotViewModel(45);
|
||||||
}
|
SlotMap[45] = offhand;
|
||||||
|
|
||||||
public void RefreshFromContainer()
|
|
||||||
{
|
|
||||||
Inventory.Container? container = Handler.GetInventory(WindowId);
|
|
||||||
if (container == null)
|
|
||||||
{
|
|
||||||
StatusText = Translations.tui_inventory_container_not_found;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title);
|
|
||||||
|
|
||||||
foreach (var kvp in _slotMap)
|
|
||||||
{
|
|
||||||
Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null;
|
|
||||||
kvp.Value.Update(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateCursorItem(container);
|
|
||||||
int itemCount = 0;
|
|
||||||
foreach (var kvp in container.Items)
|
|
||||||
{
|
|
||||||
if (kvp.Key >= 0 && !kvp.Value.IsEmpty)
|
|
||||||
itemCount++;
|
|
||||||
}
|
|
||||||
StatusText = string.Format(Translations.tui_inventory_item_count, itemCount);
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCursorItem(Inventory.Container container)
|
|
||||||
{
|
|
||||||
if (container.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty)
|
|
||||||
{
|
|
||||||
CursorItemInfo = $"x{cursorItem.Count} {cursorItem.GetTypeString()}";
|
|
||||||
HasCursorItem = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CursorItemInfo = "";
|
|
||||||
HasCursorItem = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool PerformAction(int slotId, WindowActionType action)
|
|
||||||
{
|
|
||||||
bool result = Handler.DoWindowAction(WindowId, slotId, action);
|
|
||||||
RefreshFromContainer();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue