mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Minimap support
This commit is contained in:
parent
c2475ea9e5
commit
6631180f8a
17 changed files with 5961 additions and 6 deletions
|
|
@ -215,7 +215,42 @@ The JSON maps block names (snake_case) → collision shape IDs → AABB coordina
|
||||||
|
|
||||||
**Data source**: PrismarineJS `minecraft-data` repo, path: `data/pc/<version>/blockCollisionShapes.json`. Version availability can be checked via `data/dataPaths.json`.
|
**Data source**: PrismarineJS `minecraft-data` repo, path: `data/pc/<version>/blockCollisionShapes.json`. Version availability can be checked via `data/dataPaths.json`.
|
||||||
|
|
||||||
## Step 9: Compile and Verify
|
## Step 9: Update Minimap Block Color Map
|
||||||
|
|
||||||
|
Regenerate the block-to-MapColor mapping used by the TUI minimap. This maps each block's `Material` enum to the RGB color from Minecraft's official `MapColor` table.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 $MCC_REPO/tools/gen_block_color_map.py $MCC_REPO/MinecraftOfficial/<version>-decompiled
|
||||||
|
# e.g. python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`).
|
||||||
|
|
||||||
|
The script parses `MapColor.java`, `DyeColor.java`, and `Blocks.java` from the decompiled source to extract each block's assigned map color. Blocks not matched to a known `Material` enum value are skipped.
|
||||||
|
|
||||||
|
**When to update**: Whenever new blocks are added or existing blocks change their `mapColor()` assignment. If only items or entities changed, this step can be skipped.
|
||||||
|
|
||||||
|
## Step 10: Update Minimap Entity Categories
|
||||||
|
|
||||||
|
Regenerate the entity-to-MobCategory mapping used by the TUI minimap for classifying entities as hostile, passive, neutral, or non-living.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 $MCC_REPO/tools/gen_entity_category_map.py $MCC_REPO/MinecraftOfficial/<version>-decompiled
|
||||||
|
# e.g. python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`).
|
||||||
|
|
||||||
|
The script parses `EntityType.java` to extract each entity's `MobCategory` assignment, then maps Minecraft's categories to MCC minimap categories:
|
||||||
|
- `MONSTER` -> hostile (with neutral overrides for conditionally hostile mobs like Enderman, Spider, Wolf)
|
||||||
|
- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive
|
||||||
|
- `MISC` -> non_living (with passive overrides for Villager, WanderingTrader, ZombieHorse)
|
||||||
|
|
||||||
|
The script maintains manual override lists for "neutral" mobs (attack only when provoked) since Minecraft has no machine-readable flag for this behavior. Review and update the `NEUTRAL_OVERRIDES` and `PASSIVE_OVERRIDES` sets in the script when new conditionally-hostile or misclassified mobs are added.
|
||||||
|
|
||||||
|
**When to update**: Whenever new entity types are added. If only blocks or items changed, this step can be skipped.
|
||||||
|
|
||||||
|
## Step 11: Compile and Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet build $MCC_REPO/MinecraftClient.sln -c Release
|
dotnet build $MCC_REPO/MinecraftClient.sln -c Release
|
||||||
|
|
@ -274,3 +309,5 @@ All scripts are in `$MCC_REPO/tools/`. See `tools/README.md` for detailed usage.
|
||||||
| `gen_entity_palette.py` | Generate EntityPalette C# | registries.json |
|
| `gen_entity_palette.py` | Generate EntityPalette C# | registries.json |
|
||||||
| `gen_entity_metadata_palette.py` | Generate EntityMetadataPalette C# | Decompiled source |
|
| `gen_entity_metadata_palette.py` | Generate EntityMetadataPalette C# | Decompiled source |
|
||||||
| `gen_block_shapes.py` | Download & compact block collision shapes | PrismarineJS minecraft-data |
|
| `gen_block_shapes.py` | Download & compact block collision shapes | PrismarineJS minecraft-data |
|
||||||
|
| `gen_block_color_map.py` | Generate minimap block color JSON | Decompiled source (MapColor/DyeColor/Blocks) |
|
||||||
|
| `gen_entity_category_map.py` | Generate minimap entity category JSON | Decompiled source (EntityType.java) |
|
||||||
|
|
|
||||||
256
MinecraftClient/Commands/Minimap.cs
Normal file
256
MinecraftClient/Commands/Minimap.cs
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
using System;
|
||||||
|
using Brigadier.NET;
|
||||||
|
using Brigadier.NET.Builder;
|
||||||
|
using MinecraftClient.CommandHandler;
|
||||||
|
using MinecraftClient.Tui;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using static MinecraftClient.CommandHandler.CmdResult;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Commands
|
||||||
|
{
|
||||||
|
class Minimap : Command
|
||||||
|
{
|
||||||
|
public override string CmdName => "minimap";
|
||||||
|
public override string CmdUsage => "minimap [on|off] | minimap zoom [in|out|<1-16>] | minimap names [players|hostile|neutral|passive] [on|off] | minimap names [all_on|all_off] | minimap position [top_left|top_right|center|bottom_left|bottom_right]";
|
||||||
|
public override string CmdDesc => Translations.cmd_minimap_desc;
|
||||||
|
|
||||||
|
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||||
|
{
|
||||||
|
dispatcher.Register(l => l.Literal("help")
|
||||||
|
.Then(l => l.Literal(CmdName)
|
||||||
|
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatcher.Register(l => l.Literal(CmdName)
|
||||||
|
.Executes(r => DoToggle(r.Source))
|
||||||
|
.Then(l => l.Literal("on")
|
||||||
|
.Executes(r => DoOn(r.Source)))
|
||||||
|
.Then(l => l.Literal("off")
|
||||||
|
.Executes(r => DoOff(r.Source)))
|
||||||
|
.Then(l => l.Literal("zoom")
|
||||||
|
.Executes(r => DoZoomInfo(r.Source))
|
||||||
|
.Then(l => l.Literal("in")
|
||||||
|
.Executes(r => DoZoomIn(r.Source)))
|
||||||
|
.Then(l => l.Literal("out")
|
||||||
|
.Executes(r => DoZoomOut(r.Source)))
|
||||||
|
.Then(l => l.Argument("level", Arguments.Integer(MinimapControl.MinZoom, MinimapControl.MaxZoom))
|
||||||
|
.Executes(r => DoZoomSet(r.Source, Arguments.GetInteger(r, "level")))))
|
||||||
|
.Then(l => l.Literal("names")
|
||||||
|
.Executes(r => DoNamesInfo(r.Source))
|
||||||
|
.Then(l => l.Literal("all_on")
|
||||||
|
.Executes(r => DoNamesAll(r.Source, true)))
|
||||||
|
.Then(l => l.Literal("all_off")
|
||||||
|
.Executes(r => DoNamesAll(r.Source, false)))
|
||||||
|
.Then(l => l.Literal("players")
|
||||||
|
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Player))
|
||||||
|
.Then(l => l.Literal("on")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, true)))
|
||||||
|
.Then(l => l.Literal("off")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, false))))
|
||||||
|
.Then(l => l.Literal("hostile")
|
||||||
|
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Hostile))
|
||||||
|
.Then(l => l.Literal("on")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, true)))
|
||||||
|
.Then(l => l.Literal("off")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, false))))
|
||||||
|
.Then(l => l.Literal("neutral")
|
||||||
|
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Neutral))
|
||||||
|
.Then(l => l.Literal("on")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, true)))
|
||||||
|
.Then(l => l.Literal("off")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, false))))
|
||||||
|
.Then(l => l.Literal("passive")
|
||||||
|
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Passive))
|
||||||
|
.Then(l => l.Literal("on")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, true)))
|
||||||
|
.Then(l => l.Literal("off")
|
||||||
|
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, false)))))
|
||||||
|
.Then(l => l.Literal("position")
|
||||||
|
.Executes(r => DoPositionInfo(r.Source))
|
||||||
|
.Then(l => l.Literal("top_left")
|
||||||
|
.Executes(r => DoPositionSet(r.Source, MinimapPosition.top_left)))
|
||||||
|
.Then(l => l.Literal("top_right")
|
||||||
|
.Executes(r => DoPositionSet(r.Source, MinimapPosition.top_right)))
|
||||||
|
.Then(l => l.Literal("center")
|
||||||
|
.Executes(r => DoPositionSet(r.Source, MinimapPosition.center)))
|
||||||
|
.Then(l => l.Literal("bottom_left")
|
||||||
|
.Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_left)))
|
||||||
|
.Then(l => l.Literal("bottom_right")
|
||||||
|
.Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_right))))
|
||||||
|
.Then(l => l.Literal("_help")
|
||||||
|
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||||
|
.Redirect(dispatcher.GetRoot().GetChild("help")?.GetChild(CmdName)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetUsage(CmdResult r, string _) =>
|
||||||
|
r.SetAndReturn(GetCmdDescTranslated());
|
||||||
|
|
||||||
|
private static MainTuiView? GetTuiView(CmdResult r)
|
||||||
|
{
|
||||||
|
if (ConsoleIO.Backend is not TuiConsoleBackend)
|
||||||
|
{
|
||||||
|
r.SetAndReturn(Status.Fail, Translations.cmd_minimap_tui_only);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TuiConsoleBackend.Instance?.GetView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoToggle(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
bool wasVisible = view.IsMinimapVisible;
|
||||||
|
Dispatcher.UIThread.Post(() => view.ToggleMinimap());
|
||||||
|
string msg = wasVisible
|
||||||
|
? Translations.cmd_minimap_disabled
|
||||||
|
: Translations.cmd_minimap_enabled;
|
||||||
|
return r.SetAndReturn(Status.Done, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoOn(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() => view.ShowMinimap());
|
||||||
|
return r.SetAndReturn(Status.Done, Translations.cmd_minimap_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoOff(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() => view.HideMinimap());
|
||||||
|
return r.SetAndReturn(Status.Done, Translations.cmd_minimap_disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoZoomInfo(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
int current = view.GetMinimapZoom();
|
||||||
|
return r.SetAndReturn(Status.Done,
|
||||||
|
string.Format(Translations.cmd_minimap_zoom_current, current, MinimapControl.MaxZoom));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoZoomIn(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
int newLevel = Math.Max(view.GetMinimapZoom() - 1, MinimapControl.MinZoom);
|
||||||
|
Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel));
|
||||||
|
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoZoomOut(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
int newLevel = Math.Min(view.GetMinimapZoom() + 1, MinimapControl.MaxZoom);
|
||||||
|
Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel));
|
||||||
|
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoZoomSet(CmdResult r, int level)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() => view.SetMinimapZoom(level));
|
||||||
|
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoNamesInfo(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
var nc = view.GetMinimapNameConfig();
|
||||||
|
string status = string.Format(Translations.cmd_minimap_names_status,
|
||||||
|
BoolStr(nc.Players), BoolStr(nc.Hostile), BoolStr(nc.Neutral), BoolStr(nc.Passive));
|
||||||
|
return r.SetAndReturn(Status.Done, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoNamesAll(CmdResult r, bool on)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
view.GetMinimapNameConfig().SetAll(on);
|
||||||
|
view.SyncMinimapNameConfig();
|
||||||
|
});
|
||||||
|
string msg = on ? Translations.cmd_minimap_names_all_on : Translations.cmd_minimap_names_all_off;
|
||||||
|
return r.SetAndReturn(Status.Done, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoNamesCatInfo(CmdResult r, MobCategory cat)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
var nc = view.GetMinimapNameConfig();
|
||||||
|
bool val = cat switch
|
||||||
|
{
|
||||||
|
MobCategory.Player => nc.Players,
|
||||||
|
MobCategory.Hostile => nc.Hostile,
|
||||||
|
MobCategory.Neutral => nc.Neutral,
|
||||||
|
MobCategory.Passive => nc.Passive,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
return r.SetAndReturn(Status.Done,
|
||||||
|
string.Format(Translations.cmd_minimap_names_cat, cat, BoolStr(val)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoNamesCatSet(CmdResult r, MobCategory cat, bool on)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var nc = view.GetMinimapNameConfig();
|
||||||
|
switch (cat)
|
||||||
|
{
|
||||||
|
case MobCategory.Player: nc.Players = on; break;
|
||||||
|
case MobCategory.Hostile: nc.Hostile = on; break;
|
||||||
|
case MobCategory.Neutral: nc.Neutral = on; break;
|
||||||
|
case MobCategory.Passive: nc.Passive = on; break;
|
||||||
|
}
|
||||||
|
view.SyncMinimapNameConfig();
|
||||||
|
});
|
||||||
|
return r.SetAndReturn(Status.Done,
|
||||||
|
string.Format(Translations.cmd_minimap_names_cat_set, cat, BoolStr(on)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoPositionInfo(CmdResult r)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
var pos = view.GetMinimapPosition();
|
||||||
|
return r.SetAndReturn(Status.Done,
|
||||||
|
string.Format(Translations.cmd_minimap_position_current, pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DoPositionSet(CmdResult r, MinimapPosition pos)
|
||||||
|
{
|
||||||
|
var view = GetTuiView(r);
|
||||||
|
if (view is null) return (int)r.status;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() => view.SetMinimapPosition(pos));
|
||||||
|
return r.SetAndReturn(Status.Done,
|
||||||
|
string.Format(Translations.cmd_minimap_position_set, pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BoolStr(bool v) => v ? "ON" : "OFF";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,8 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Physics\BlockShapeData.json" LogicalName="BlockShapeData.json" />
|
<EmbeddedResource Include="Physics\BlockShapeData.json" LogicalName="BlockShapeData.json" />
|
||||||
|
<EmbeddedResource Include="Tui\MinimapBlockColors.json" LogicalName="MinimapBlockColors.json" />
|
||||||
|
<EmbeddedResource Include="Tui\MinimapEntityCategories.json" LogicalName="MinimapEntityCategories.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Protocol\Handlers\Compression\**" />
|
<Compile Remove="Protocol\Handlers\Compression\**" />
|
||||||
|
|
|
||||||
|
|
@ -664,8 +664,10 @@ namespace MinecraftClient.Protocol.Handlers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
|
var entity = new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
|
||||||
data);
|
data);
|
||||||
|
entity.UUID = entityUUID;
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -933,6 +933,39 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
|
||||||
<data name="Main.Advanced.enable_sentry" xml:space="preserve">
|
<data name="Main.Advanced.enable_sentry" xml:space="preserve">
|
||||||
<value>Set to false to opt-out of Sentry error logging.</value>
|
<value>Set to false to opt-out of Sentry error logging.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Console.Minimap" xml:space="preserve">
|
||||||
|
<value>Settings for the TUI minimap overlay that shows terrain and entities.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.Enabled" xml:space="preserve">
|
||||||
|
<value>Whether the minimap is visible on startup in TUI mode.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.Zoom" xml:space="preserve">
|
||||||
|
<value>Blocks per pixel, 1-16. 1 = closest (1:1), 16 = farthest (16 blocks per pixel).</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.Width" xml:space="preserve">
|
||||||
|
<value>Map width in pixels (characters). Range 10-120, default 40.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.Height" xml:space="preserve">
|
||||||
|
<value>Map height in pixels (must be even, uses half-block chars). Range 4-80, default 40.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.Position" xml:space="preserve">
|
||||||
|
<value>Minimap position: "top_left", "top_right", "center", "bottom_left", or "bottom_right".</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.ShowPlayerNames" xml:space="preserve">
|
||||||
|
<value>Show player names on the minimap.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.ShowHostileNames" xml:space="preserve">
|
||||||
|
<value>Show hostile mob names on the minimap.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.ShowNeutralNames" xml:space="preserve">
|
||||||
|
<value>Show neutral mob names on the minimap.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.ShowPassiveNames" xml:space="preserve">
|
||||||
|
<value>Show passive mob names on the minimap.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Console.Minimap.RefreshInterval" xml:space="preserve">
|
||||||
|
<value>Minimap refresh interval in milliseconds (200-5000, default 1000).</value>
|
||||||
|
</data>
|
||||||
<data name="Main.General.AuthlibUser" xml:space="preserve">
|
<data name="Main.General.AuthlibUser" xml:space="preserve">
|
||||||
<value>Yggdrasil authlib multi-user selection.</value>
|
<value>Yggdrasil authlib multi-user selection.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -6880,5 +6880,158 @@ namespace MinecraftClient {
|
||||||
return ResourceManager.GetString("tui.crafting.grid", resourceCulture);
|
return ResourceManager.GetString("tui.crafting.grid", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Toggle the TUI minimap overlay, or adjust its zoom level..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_desc {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.desc", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Minimap enabled..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_enabled {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.enabled", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Minimap disabled..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_disabled {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.disabled", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Minimap zoom set to {0}:1 (blocks per pixel)..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_zoom_set {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.zoom_set", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Current minimap zoom: {0}:1 blocks/px (range 1-{1})..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_zoom_current {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.zoom_current", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The minimap command is only available in TUI mode..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_tui_only {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.tui_only", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Hostile.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_minimap_legend_hostile {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.minimap.legend.hostile", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Passive.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_minimap_legend_passive {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.minimap.legend.passive", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Neutral.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_minimap_legend_neutral {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.minimap.legend.neutral", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Player.
|
||||||
|
/// </summary>
|
||||||
|
internal static string tui_minimap_legend_player {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("tui.minimap.legend.player", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_names_status {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.names_status", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to All entity name labels enabled..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_names_all_on {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.names_all_on", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to All entity name labels disabled..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_names_all_off {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.names_all_off", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} name display: {1}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_names_cat {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.names_cat", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} name display set to {1}..
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_names_cat_set {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.names_cat_set", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Current minimap position: {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_position_current {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.position_current", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Minimap position set to: {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string cmd_minimap_position_set {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("cmd.minimap.position_set", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2419,4 +2419,55 @@ see item details.</value>
|
||||||
<data name="tui.crafting.grid" xml:space="preserve">
|
<data name="tui.crafting.grid" xml:space="preserve">
|
||||||
<value>Crafting</value>
|
<value>Crafting</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="cmd.minimap.desc" xml:space="preserve">
|
||||||
|
<value>Toggle the TUI minimap overlay, or adjust its zoom level.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.enabled" xml:space="preserve">
|
||||||
|
<value>Minimap enabled.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.disabled" xml:space="preserve">
|
||||||
|
<value>Minimap disabled.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.zoom_set" xml:space="preserve">
|
||||||
|
<value>Minimap zoom set to {0}:1 (blocks per pixel).</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.zoom_current" xml:space="preserve">
|
||||||
|
<value>Current minimap zoom: {0}:1 blocks/px (range 1-{1}).</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.tui_only" xml:space="preserve">
|
||||||
|
<value>The minimap command is only available in TUI mode.</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.minimap.legend.hostile" xml:space="preserve">
|
||||||
|
<value>Hostile</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.minimap.legend.passive" xml:space="preserve">
|
||||||
|
<value>Passive</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.minimap.legend.neutral" xml:space="preserve">
|
||||||
|
<value>Neutral</value>
|
||||||
|
</data>
|
||||||
|
<data name="tui.minimap.legend.player" xml:space="preserve">
|
||||||
|
<value>Player</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.names_status" xml:space="preserve">
|
||||||
|
<value>Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.names_all_on" xml:space="preserve">
|
||||||
|
<value>All entity name labels enabled.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.names_all_off" xml:space="preserve">
|
||||||
|
<value>All entity name labels disabled.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.names_cat" xml:space="preserve">
|
||||||
|
<value>{0} name display: {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.names_cat_set" xml:space="preserve">
|
||||||
|
<value>{0} name display set to {1}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.position_current" xml:space="preserve">
|
||||||
|
<value>Current minimap position: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="cmd.minimap.position_set" xml:space="preserve">
|
||||||
|
<value>Minimap position set to: {0}</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
||||||
|
|
@ -1108,6 +1108,9 @@ namespace MinecraftClient
|
||||||
[TomlPrecedingComment("$Console.CommandSuggestion$")]
|
[TomlPrecedingComment("$Console.CommandSuggestion$")]
|
||||||
public CommandSuggestionConfig CommandSuggestion = new();
|
public CommandSuggestionConfig CommandSuggestion = new();
|
||||||
|
|
||||||
|
[TomlPrecedingComment("$Console.Minimap$")]
|
||||||
|
public MinimapConfig Minimap = new();
|
||||||
|
|
||||||
public void OnSettingUpdate()
|
public void OnSettingUpdate()
|
||||||
{
|
{
|
||||||
var backend = ConsoleIO.Backend;
|
var backend = ConsoleIO.Backend;
|
||||||
|
|
@ -1246,6 +1249,50 @@ namespace MinecraftClient
|
||||||
|
|
||||||
public enum ConsoleModeType { classic, tui };
|
public enum ConsoleModeType { classic, tui };
|
||||||
public enum ConsoleColorModeType { disable, legacy_4bit, vt100_4bit, vt100_8bit, vt100_24bit };
|
public enum ConsoleColorModeType { disable, legacy_4bit, vt100_4bit, vt100_8bit, vt100_24bit };
|
||||||
|
|
||||||
|
[TomlDoNotInlineObject]
|
||||||
|
public class MinimapConfig
|
||||||
|
{
|
||||||
|
[TomlInlineComment("$Console.Minimap.Enabled$")]
|
||||||
|
public bool Enabled = false;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.Zoom$")]
|
||||||
|
public int Zoom = Tui.MinimapControl.DefaultZoom;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.Width$")]
|
||||||
|
public int Width = Tui.MinimapControl.DefaultWidth;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.Height$")]
|
||||||
|
public int Height = Tui.MinimapControl.DefaultHeight;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.Position$")]
|
||||||
|
public Tui.MinimapPosition Position = Tui.MinimapPosition.top_right;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.ShowPlayerNames$")]
|
||||||
|
public bool ShowPlayerNames = false;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.ShowHostileNames$")]
|
||||||
|
public bool ShowHostileNames = false;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.ShowNeutralNames$")]
|
||||||
|
public bool ShowNeutralNames = false;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.ShowPassiveNames$")]
|
||||||
|
public bool ShowPassiveNames = false;
|
||||||
|
|
||||||
|
[TomlInlineComment("$Console.Minimap.RefreshInterval$")]
|
||||||
|
public int RefreshInterval = Tui.MinimapControl.DefaultRefreshMs;
|
||||||
|
|
||||||
|
public void OnSettingUpdate()
|
||||||
|
{
|
||||||
|
Zoom = Math.Clamp(Zoom, Tui.MinimapControl.MinZoom, Tui.MinimapControl.MaxZoom);
|
||||||
|
Width = Math.Clamp(Width, 10, 120);
|
||||||
|
Height = Math.Clamp(Height, 4, 80);
|
||||||
|
if (Height % 2 != 0) Height++;
|
||||||
|
RefreshInterval = Math.Clamp(RefreshInterval,
|
||||||
|
Tui.MinimapControl.MinRefreshMs, Tui.MinimapControl.MaxRefreshMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ namespace MinecraftClient.Tui
|
||||||
private long _lastLogClickTicks;
|
private long _lastLogClickTicks;
|
||||||
private const int DoubleClickMsec = 500;
|
private const int DoubleClickMsec = 500;
|
||||||
|
|
||||||
|
private readonly Border _minimapBorder;
|
||||||
|
private readonly MinimapControl _minimapControl;
|
||||||
|
private volatile bool _minimapVisible;
|
||||||
|
|
||||||
private readonly Border _suggestionBorder;
|
private readonly Border _suggestionBorder;
|
||||||
private readonly StackPanel _suggestionPanel;
|
private readonly StackPanel _suggestionPanel;
|
||||||
private CommandSuggestion[] _suggestions = Array.Empty<CommandSuggestion>();
|
private CommandSuggestion[] _suggestions = Array.Empty<CommandSuggestion>();
|
||||||
|
|
@ -150,6 +154,29 @@ namespace MinecraftClient.Tui
|
||||||
Margin = new Thickness(0, 0, 0, 1),
|
Margin = new Thickness(0, 0, 0, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var mmCfg = Settings.Config.Console.Minimap;
|
||||||
|
mmCfg.OnSettingUpdate();
|
||||||
|
_minimapControl = new MinimapControl(mmCfg.Width, mmCfg.Height);
|
||||||
|
_minimapControl.BlocksPerPixel = mmCfg.Zoom;
|
||||||
|
_minimapControl.RefreshIntervalMs = mmCfg.RefreshInterval;
|
||||||
|
_minimapControl.NameConfig.Players = mmCfg.ShowPlayerNames;
|
||||||
|
_minimapControl.NameConfig.Hostile = mmCfg.ShowHostileNames;
|
||||||
|
_minimapControl.NameConfig.Neutral = mmCfg.ShowNeutralNames;
|
||||||
|
_minimapControl.NameConfig.Passive = mmCfg.ShowPassiveNames;
|
||||||
|
|
||||||
|
var (hAlign, vAlign, margin) = GetMinimapAlignment(mmCfg.Position);
|
||||||
|
_minimapBorder = new Border
|
||||||
|
{
|
||||||
|
Background = new SolidColorBrush(Color.FromArgb(220, 15, 15, 15)),
|
||||||
|
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Child = _minimapControl,
|
||||||
|
IsVisible = false,
|
||||||
|
HorizontalAlignment = hAlign,
|
||||||
|
VerticalAlignment = vAlign,
|
||||||
|
Margin = margin,
|
||||||
|
};
|
||||||
|
|
||||||
_mainContent = new DockPanel
|
_mainContent = new DockPanel
|
||||||
{
|
{
|
||||||
Background = Brushes.Black,
|
Background = Brushes.Black,
|
||||||
|
|
@ -164,11 +191,18 @@ namespace MinecraftClient.Tui
|
||||||
_rootPanel = new Panel
|
_rootPanel = new Panel
|
||||||
{
|
{
|
||||||
Background = Brushes.Black,
|
Background = Brushes.Black,
|
||||||
Children = { _mainContent, _notificationBorder, _suggestionBorder }
|
Children = { _mainContent, _minimapBorder, _notificationBorder, _suggestionBorder }
|
||||||
};
|
};
|
||||||
|
|
||||||
Content = _rootPanel;
|
Content = _rootPanel;
|
||||||
|
|
||||||
|
if (mmCfg.Enabled)
|
||||||
|
{
|
||||||
|
_minimapVisible = true;
|
||||||
|
_minimapBorder.IsVisible = true;
|
||||||
|
_minimapControl.Start();
|
||||||
|
}
|
||||||
|
|
||||||
StartStatusBarTimer();
|
StartStatusBarTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -986,6 +1020,95 @@ namespace MinecraftClient.Tui
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Minimap
|
||||||
|
|
||||||
|
public void ShowMinimap()
|
||||||
|
{
|
||||||
|
if (_minimapVisible) return;
|
||||||
|
_minimapVisible = true;
|
||||||
|
_minimapBorder.IsVisible = true;
|
||||||
|
_minimapControl.Start();
|
||||||
|
Settings.Config.Console.Minimap.Enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideMinimap()
|
||||||
|
{
|
||||||
|
if (!_minimapVisible) return;
|
||||||
|
_minimapVisible = false;
|
||||||
|
_minimapControl.Stop();
|
||||||
|
_minimapBorder.IsVisible = false;
|
||||||
|
Settings.Config.Console.Minimap.Enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleMinimap()
|
||||||
|
{
|
||||||
|
if (_minimapVisible)
|
||||||
|
HideMinimap();
|
||||||
|
else
|
||||||
|
ShowMinimap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMinimapVisible => _minimapVisible;
|
||||||
|
|
||||||
|
public void SetMinimapZoom(int level)
|
||||||
|
{
|
||||||
|
_minimapControl.BlocksPerPixel = level;
|
||||||
|
Settings.Config.Console.Minimap.Zoom = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetMinimapZoom() => _minimapControl.BlocksPerPixel;
|
||||||
|
|
||||||
|
public NameDisplayConfig GetMinimapNameConfig() => _minimapControl.NameConfig;
|
||||||
|
|
||||||
|
public void SyncMinimapNameConfig()
|
||||||
|
{
|
||||||
|
var nc = _minimapControl.NameConfig;
|
||||||
|
var cfg = Settings.Config.Console.Minimap;
|
||||||
|
cfg.ShowPlayerNames = nc.Players;
|
||||||
|
cfg.ShowHostileNames = nc.Hostile;
|
||||||
|
cfg.ShowNeutralNames = nc.Neutral;
|
||||||
|
cfg.ShowPassiveNames = nc.Passive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResizeMinimap(int width, int height)
|
||||||
|
{
|
||||||
|
_minimapControl.Resize(width, height);
|
||||||
|
Settings.Config.Console.Minimap.Width = width;
|
||||||
|
Settings.Config.Console.Minimap.Height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMinimapPosition(MinimapPosition pos)
|
||||||
|
{
|
||||||
|
var (hAlign, vAlign, margin) = GetMinimapAlignment(pos);
|
||||||
|
_minimapBorder.HorizontalAlignment = hAlign;
|
||||||
|
_minimapBorder.VerticalAlignment = vAlign;
|
||||||
|
_minimapBorder.Margin = margin;
|
||||||
|
Settings.Config.Console.Minimap.Position = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MinimapPosition GetMinimapPosition() => Settings.Config.Console.Minimap.Position;
|
||||||
|
|
||||||
|
private static (HorizontalAlignment h, VerticalAlignment v, Thickness margin) GetMinimapAlignment(MinimapPosition pos) => pos switch
|
||||||
|
{
|
||||||
|
MinimapPosition.top_left => (HorizontalAlignment.Left, VerticalAlignment.Top, new Thickness(1, 1, 0, 0)),
|
||||||
|
MinimapPosition.top_right => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)),
|
||||||
|
MinimapPosition.center => (HorizontalAlignment.Center, VerticalAlignment.Center, new Thickness(0)),
|
||||||
|
MinimapPosition.bottom_left => (HorizontalAlignment.Left, VerticalAlignment.Bottom, new Thickness(1, 0, 0, 2)),
|
||||||
|
MinimapPosition.bottom_right => (HorizontalAlignment.Right, VerticalAlignment.Bottom, new Thickness(0, 0, 1, 2)),
|
||||||
|
_ => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
public void ApplyMinimapConfig()
|
||||||
|
{
|
||||||
|
var cfg = Settings.Config.Console.Minimap;
|
||||||
|
if (cfg.Enabled && !_minimapVisible)
|
||||||
|
ShowMinimap();
|
||||||
|
else if (!cfg.Enabled && _minimapVisible)
|
||||||
|
HideMinimap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Overlay
|
#region Overlay
|
||||||
|
|
||||||
public void ShowOverlay(Control content, Action? onClose = null)
|
public void ShowOverlay(Control content, Action? onClose = null)
|
||||||
|
|
|
||||||
3552
MinecraftClient/Tui/MinimapBlockColors.json
Normal file
3552
MinecraftClient/Tui/MinimapBlockColors.json
Normal file
File diff suppressed because it is too large
Load diff
168
MinecraftClient/Tui/MinimapColorMap.cs
Normal file
168
MinecraftClient/Tui/MinimapColorMap.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Mapping;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maps block Materials to minimap colors using data extracted from Minecraft's
|
||||||
|
/// official MapColor table. Colors are loaded from the embedded MinimapBlockColors.json
|
||||||
|
/// resource generated by tools/gen_block_color_map.py.
|
||||||
|
/// </summary>
|
||||||
|
public static class MinimapColorMap
|
||||||
|
{
|
||||||
|
public static readonly Color WaterColor = Color.FromRgb(64, 64, 255);
|
||||||
|
public static readonly Color IceColor = Color.FromRgb(160, 160, 255);
|
||||||
|
public static readonly Color LavaColor = Color.FromRgb(255, 100, 0);
|
||||||
|
public static readonly Color DefaultColor = Color.FromRgb(60, 60, 60);
|
||||||
|
public static readonly Color VoidColor = Color.FromRgb(0, 0, 0);
|
||||||
|
|
||||||
|
private static readonly FrozenDictionary<Material, Color> ColorTable;
|
||||||
|
private static readonly FrozenSet<Material> FullyTransparentMats;
|
||||||
|
private static readonly FrozenSet<Material> WaterMats;
|
||||||
|
private static readonly FrozenSet<Material> IceMats;
|
||||||
|
|
||||||
|
static MinimapColorMap()
|
||||||
|
{
|
||||||
|
var colors = new Dictionary<Material, Color>();
|
||||||
|
var transparent = new HashSet<Material>();
|
||||||
|
var water = new HashSet<Material>();
|
||||||
|
var ice = new HashSet<Material>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceStream("MinimapBlockColors.json");
|
||||||
|
if (stream is not null)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(stream);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("colors", out var colorsEl))
|
||||||
|
{
|
||||||
|
foreach (var prop in colorsEl.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<Material>(prop.Name, out var mat))
|
||||||
|
continue;
|
||||||
|
var arr = prop.Value;
|
||||||
|
if (arr.GetArrayLength() < 3) continue;
|
||||||
|
byte r = (byte)arr[0].GetInt32();
|
||||||
|
byte g = (byte)arr[1].GetInt32();
|
||||||
|
byte b = (byte)arr[2].GetInt32();
|
||||||
|
colors[mat] = Color.FromRgb(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("transparent", out var transEl))
|
||||||
|
{
|
||||||
|
foreach (var item in transEl.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||||
|
transparent.Add(mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("water", out var waterEl))
|
||||||
|
{
|
||||||
|
foreach (var item in waterEl.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||||
|
water.Add(mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("ice", out var iceEl))
|
||||||
|
{
|
||||||
|
foreach (var item in iceEl.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||||
|
ice.Add(mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Failed to load color data: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transparent.Count == 0)
|
||||||
|
{
|
||||||
|
transparent.Add(Material.Air);
|
||||||
|
transparent.Add(Material.CaveAir);
|
||||||
|
transparent.Add(Material.VoidAir);
|
||||||
|
}
|
||||||
|
if (water.Count == 0)
|
||||||
|
water.Add(Material.Water);
|
||||||
|
if (ice.Count == 0)
|
||||||
|
{
|
||||||
|
ice.Add(Material.Ice);
|
||||||
|
ice.Add(Material.PackedIce);
|
||||||
|
ice.Add(Material.BlueIce);
|
||||||
|
ice.Add(Material.FrostedIce);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorTable = colors.ToFrozenDictionary();
|
||||||
|
FullyTransparentMats = transparent.ToFrozenSet();
|
||||||
|
WaterMats = water.ToFrozenSet();
|
||||||
|
IceMats = ice.ToFrozenSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsFullyTransparent(Material m) => FullyTransparentMats.Contains(m);
|
||||||
|
|
||||||
|
public static bool IsWater(Material m) => WaterMats.Contains(m);
|
||||||
|
|
||||||
|
public static bool IsIce(Material m) => IceMats.Contains(m);
|
||||||
|
|
||||||
|
public static Color GetBaseColor(Material m)
|
||||||
|
{
|
||||||
|
if (m == Material.Lava)
|
||||||
|
return LavaColor;
|
||||||
|
return ColorTable.GetValueOrDefault(m, DefaultColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply Minecraft-style height shading. The shade multiplier depends on
|
||||||
|
/// the height difference between the current block and the block to its north.
|
||||||
|
/// Vanilla maps use four brightness levels: LOW (180/255), NORMAL (220/255),
|
||||||
|
/// HIGH (255/255), and LOWEST (135/255). We use NORMAL as baseline and shift
|
||||||
|
/// up/down based on delta.
|
||||||
|
/// </summary>
|
||||||
|
public static Color ApplyHeightShade(Color baseColor, int heightDelta)
|
||||||
|
{
|
||||||
|
int multiplier = heightDelta switch
|
||||||
|
{
|
||||||
|
> 0 => 255, // higher than neighbor: brightest
|
||||||
|
0 => 220, // same height: normal
|
||||||
|
_ => 180, // lower than neighbor: darker
|
||||||
|
};
|
||||||
|
byte r = (byte)(baseColor.R * multiplier / 255);
|
||||||
|
byte g = (byte)(baseColor.G * multiplier / 255);
|
||||||
|
byte b = (byte)(baseColor.B * multiplier / 255);
|
||||||
|
return Color.FromRgb(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color BlendWaterColor(Color bottomColor, int waterDepth)
|
||||||
|
{
|
||||||
|
double alpha = Math.Min(0.85, 0.35 + waterDepth * 0.08);
|
||||||
|
return Blend(WaterColor, bottomColor, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color BlendIceColor(Color bottomColor)
|
||||||
|
{
|
||||||
|
return Blend(IceColor, bottomColor, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color Blend(Color top, Color bottom, double topAlpha)
|
||||||
|
{
|
||||||
|
byte r = (byte)(top.R * topAlpha + bottom.R * (1.0 - topAlpha));
|
||||||
|
byte g = (byte)(top.G * topAlpha + bottom.G * (1.0 - topAlpha));
|
||||||
|
byte b = (byte)(top.B * topAlpha + bottom.B * (1.0 - topAlpha));
|
||||||
|
return Color.FromRgb(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
677
MinecraftClient/Tui/MinimapControl.cs
Normal file
677
MinecraftClient/Tui/MinimapControl.cs
Normal file
|
|
@ -0,0 +1,677 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using MinecraftClient.Mapping;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TUI minimap control rendered as a grid of TextBlocks using half-block characters.
|
||||||
|
/// Zoom is expressed as blocks-per-pixel (1 = 1:1, 16 = 16 blocks per pixel).
|
||||||
|
/// Entity names are drawn directly on the map below their icon.
|
||||||
|
/// </summary>
|
||||||
|
public class MinimapControl : UserControl
|
||||||
|
{
|
||||||
|
public const int MinZoom = 1;
|
||||||
|
public const int MaxZoom = 16;
|
||||||
|
public const int DefaultZoom = 2;
|
||||||
|
public const int DefaultWidth = 40;
|
||||||
|
public const int DefaultHeight = 40;
|
||||||
|
public const int DefaultRefreshMs = 1000;
|
||||||
|
public const int MinRefreshMs = 100;
|
||||||
|
public const int MaxRefreshMs = 5000;
|
||||||
|
|
||||||
|
private int _mapWidth;
|
||||||
|
private int _mapHeight;
|
||||||
|
private int _cellRows;
|
||||||
|
|
||||||
|
private int _blocksPerPixel = DefaultZoom;
|
||||||
|
private volatile bool _sampling;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
|
private readonly NameDisplayConfig _nameConfig = new();
|
||||||
|
|
||||||
|
private TextBlock[,] _cells;
|
||||||
|
private readonly StackPanel _infoRow;
|
||||||
|
private readonly StackPanel _legendPanel;
|
||||||
|
private readonly Grid _mapGrid;
|
||||||
|
private readonly DispatcherTimer _timer;
|
||||||
|
|
||||||
|
public int BlocksPerPixel
|
||||||
|
{
|
||||||
|
get => _blocksPerPixel;
|
||||||
|
set => _blocksPerPixel = Math.Clamp(value, MinZoom, MaxZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NameDisplayConfig NameConfig => _nameConfig;
|
||||||
|
|
||||||
|
public int MapPixelWidth => _mapWidth;
|
||||||
|
public int MapPixelHeight => _mapHeight;
|
||||||
|
|
||||||
|
public int RefreshIntervalMs
|
||||||
|
{
|
||||||
|
get => (int)_timer.Interval.TotalMilliseconds;
|
||||||
|
set => _timer.Interval = TimeSpan.FromMilliseconds(Math.Clamp(value, MinRefreshMs, MaxRefreshMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public MinimapControl() : this(DefaultWidth, DefaultHeight) { }
|
||||||
|
|
||||||
|
public MinimapControl(int width, int height)
|
||||||
|
{
|
||||||
|
_mapWidth = Math.Max(10, width);
|
||||||
|
_mapHeight = Math.Max(4, height % 2 == 0 ? height : height + 1);
|
||||||
|
_cellRows = _mapHeight / 2;
|
||||||
|
|
||||||
|
_mapGrid = new Grid();
|
||||||
|
_cells = BuildGrid(_mapGrid, _cellRows, _mapWidth);
|
||||||
|
|
||||||
|
_infoRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
_legendPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
|
||||||
|
var root = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Vertical,
|
||||||
|
Children = { _mapGrid, _infoRow, _legendPanel },
|
||||||
|
};
|
||||||
|
|
||||||
|
Content = root;
|
||||||
|
|
||||||
|
_timer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromMilliseconds(DefaultRefreshMs),
|
||||||
|
};
|
||||||
|
_timer.Tick += (_, _) => RequestSample();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Resize(int width, int height)
|
||||||
|
{
|
||||||
|
_mapWidth = Math.Max(10, width);
|
||||||
|
_mapHeight = Math.Max(4, height % 2 == 0 ? height : height + 1);
|
||||||
|
_cellRows = _mapHeight / 2;
|
||||||
|
|
||||||
|
_mapGrid.Children.Clear();
|
||||||
|
_mapGrid.RowDefinitions.Clear();
|
||||||
|
_mapGrid.ColumnDefinitions.Clear();
|
||||||
|
_cells = BuildGrid(_mapGrid, _cellRows, _mapWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextBlock[,] BuildGrid(Grid grid, int rows, int cols)
|
||||||
|
{
|
||||||
|
var cells = new TextBlock[rows, cols];
|
||||||
|
for (int r = 0; r < rows; r++)
|
||||||
|
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
|
for (int c = 0; c < cols; c++)
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||||
|
|
||||||
|
for (int r = 0; r < rows; r++)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < cols; c++)
|
||||||
|
{
|
||||||
|
var tb = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\u2580",
|
||||||
|
Foreground = Brushes.Black,
|
||||||
|
Background = Brushes.Black,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = new Thickness(0),
|
||||||
|
FontSize = 1,
|
||||||
|
};
|
||||||
|
Grid.SetRow(tb, r);
|
||||||
|
Grid.SetColumn(tb, c);
|
||||||
|
grid.Children.Add(tb);
|
||||||
|
cells[r, c] = tb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_timer.Start();
|
||||||
|
RequestSample();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_timer.Stop();
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RequestSample()
|
||||||
|
{
|
||||||
|
if (_sampling) return;
|
||||||
|
if (McClient.Instance is not McClient client) return;
|
||||||
|
if (!client.GetTerrainEnabled()) return;
|
||||||
|
|
||||||
|
_sampling = true;
|
||||||
|
var ct = _cts?.Token ?? CancellationToken.None;
|
||||||
|
int bpp = _blocksPerPixel;
|
||||||
|
int w = _mapWidth;
|
||||||
|
int h = _mapHeight;
|
||||||
|
|
||||||
|
bool showPlayers = _nameConfig.Players;
|
||||||
|
bool showHostile = _nameConfig.Hostile;
|
||||||
|
bool showNeutral = _nameConfig.Neutral;
|
||||||
|
bool showPassive = _nameConfig.Passive;
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = SampleTerrain(client, bpp, w, h,
|
||||||
|
showPlayers, showHostile, showNeutral, showPassive, ct);
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
ApplyPixelBuffer(result, w, h);
|
||||||
|
UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Sample error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_sampling = false;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class EntityLabel
|
||||||
|
{
|
||||||
|
public string Name = "";
|
||||||
|
public Color LabelColor;
|
||||||
|
public int PixelX;
|
||||||
|
public int PixelY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SampleResult
|
||||||
|
{
|
||||||
|
public Color[,] Pixels = null!;
|
||||||
|
public (char Ch, Color Fg, Color Bg)?[,] CharOverlay = null!;
|
||||||
|
public HashSet<MobCategory> VisibleCategories = [];
|
||||||
|
public int[,] Heights = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldShowNameLocal(MobCategory cat,
|
||||||
|
bool showPlayers, bool showHostile, bool showNeutral, bool showPassive)
|
||||||
|
{
|
||||||
|
return cat switch
|
||||||
|
{
|
||||||
|
MobCategory.Player => showPlayers,
|
||||||
|
MobCategory.Hostile => showHostile,
|
||||||
|
MobCategory.Neutral => showNeutral,
|
||||||
|
MobCategory.Passive => showPassive,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SampleResult SampleTerrain(McClient client, int bpp, int mapW, int mapH,
|
||||||
|
bool showPlayers, bool showHostile, bool showNeutral, bool showPassive,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = new SampleResult
|
||||||
|
{
|
||||||
|
Pixels = new Color[mapW, mapH],
|
||||||
|
CharOverlay = new (char, Color, Color)?[mapW, mapH / 2],
|
||||||
|
Heights = new int[mapW, mapH],
|
||||||
|
};
|
||||||
|
var world = client.GetWorld();
|
||||||
|
var playerLoc = client.GetCurrentLocation();
|
||||||
|
|
||||||
|
int playerBlockX = (int)Math.Floor(playerLoc.X);
|
||||||
|
int playerBlockZ = (int)Math.Floor(playerLoc.Z);
|
||||||
|
int playerBlockY = (int)Math.Floor(playerLoc.Y);
|
||||||
|
|
||||||
|
var dim = World.GetDimension();
|
||||||
|
int minY = dim.minY;
|
||||||
|
int scanTop = Math.Min(playerBlockY + 32, dim.maxY - 1);
|
||||||
|
|
||||||
|
var entities = client.GetEntityHandlingEnabled()
|
||||||
|
? client.GetEntities()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var entityPixels = new Dictionary<(int, int), (Color Color, int Priority)>();
|
||||||
|
int centerX = mapW / 2;
|
||||||
|
int centerY = mapH / 2;
|
||||||
|
|
||||||
|
var nameLabels = new List<EntityLabel>();
|
||||||
|
var uuidNameMap = client.GetOnlinePlayersWithUUID();
|
||||||
|
|
||||||
|
if (entities is not null)
|
||||||
|
{
|
||||||
|
int playerEntityId = client.GetPlayerEntityID();
|
||||||
|
foreach (var kvp in entities)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) return result;
|
||||||
|
var entity = kvp.Value;
|
||||||
|
var cat = MinimapEntityClassifier.Classify(entity.Type);
|
||||||
|
if (cat == MobCategory.NonLiving) continue;
|
||||||
|
if (kvp.Key == playerEntityId) continue;
|
||||||
|
|
||||||
|
if (!MinimapEntityClassifier.ShouldDisplay(cat, playerLoc.Y, entity.Location.Y))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
double relX = (entity.Location.X - playerLoc.X) / bpp;
|
||||||
|
double relZ = (entity.Location.Z - playerLoc.Z) / bpp;
|
||||||
|
int px = (int)Math.Floor(relX) + centerX;
|
||||||
|
int py = (int)Math.Floor(relZ) + centerY;
|
||||||
|
|
||||||
|
if (px < 0 || px >= mapW || py < 0 || py >= mapH) continue;
|
||||||
|
|
||||||
|
var baseColor = MinimapEntityClassifier.GetBaseColor(cat);
|
||||||
|
Color color;
|
||||||
|
if (cat == MobCategory.Player)
|
||||||
|
color = baseColor;
|
||||||
|
else
|
||||||
|
color = MinimapEntityClassifier.ApplyDepthFade(baseColor, playerLoc.Y, entity.Location.Y);
|
||||||
|
int priority = MinimapEntityClassifier.GetPriority(cat);
|
||||||
|
|
||||||
|
var key = (px, py);
|
||||||
|
if (!entityPixels.TryGetValue(key, out var existing) || priority > existing.Priority)
|
||||||
|
entityPixels[key] = (color, priority);
|
||||||
|
|
||||||
|
result.VisibleCategories.Add(cat);
|
||||||
|
|
||||||
|
if (ShouldShowNameLocal(cat, showPlayers, showHostile, showNeutral, showPassive))
|
||||||
|
{
|
||||||
|
string name = ResolveEntityName(client, entity, cat, uuidNameMap);
|
||||||
|
nameLabels.Add(new EntityLabel
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
LabelColor = color,
|
||||||
|
PixelX = px,
|
||||||
|
PixelY = py,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityPixels[(centerX, centerY)] = (MinimapEntityClassifier.PlayerColor, 5);
|
||||||
|
result.VisibleCategories.Add(MobCategory.Player);
|
||||||
|
|
||||||
|
ChunkColumn? cachedColumn = null;
|
||||||
|
int cachedChunkX = int.MinValue, cachedChunkZ = int.MinValue;
|
||||||
|
|
||||||
|
for (int px = 0; px < mapW; px++)
|
||||||
|
{
|
||||||
|
for (int py = 0; py < mapH; py++)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) return result;
|
||||||
|
|
||||||
|
int baseX = playerBlockX + (px - centerX) * bpp;
|
||||||
|
int baseZ = playerBlockZ + (py - centerY) * bpp;
|
||||||
|
|
||||||
|
if (bpp == 1)
|
||||||
|
{
|
||||||
|
var (color, surfY) = SampleColumn(world, baseX, baseZ, scanTop, minY,
|
||||||
|
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||||
|
result.Pixels[px, py] = color;
|
||||||
|
result.Heights[px, py] = surfY;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var (color, surfY) = SampleAreaDominant(world, baseX, baseZ, bpp, scanTop, minY,
|
||||||
|
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||||
|
result.Pixels[px, py] = color;
|
||||||
|
result.Heights[px, py] = surfY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int px = 0; px < mapW; px++)
|
||||||
|
{
|
||||||
|
for (int py = 0; py < mapH; py++)
|
||||||
|
{
|
||||||
|
if (entityPixels.ContainsKey((px, py))) continue;
|
||||||
|
|
||||||
|
int northHeight = py > 0 ? result.Heights[px, py - 1] : result.Heights[px, py];
|
||||||
|
int delta = result.Heights[px, py] - northHeight;
|
||||||
|
result.Pixels[px, py] = MinimapColorMap.ApplyHeightShade(result.Pixels[px, py], delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (key, info) in entityPixels)
|
||||||
|
{
|
||||||
|
var (px, py) = key;
|
||||||
|
if (px >= 0 && px < mapW && py >= 0 && py < mapH)
|
||||||
|
result.Pixels[px, py] = info.Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
BakeNameLabels(result, nameLabels, mapW, mapH);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveEntityName(McClient client, Entity entity,
|
||||||
|
MobCategory cat, Dictionary<string, string>? uuidNameMap)
|
||||||
|
{
|
||||||
|
if (cat == MobCategory.Player)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(entity.Name))
|
||||||
|
return entity.Name;
|
||||||
|
|
||||||
|
if (entity.UUID != System.Guid.Empty)
|
||||||
|
{
|
||||||
|
var playerInfo = client.GetPlayerInfo(entity.UUID);
|
||||||
|
if (!string.IsNullOrWhiteSpace(playerInfo?.Name))
|
||||||
|
return playerInfo.Name;
|
||||||
|
|
||||||
|
if (uuidNameMap is not null &&
|
||||||
|
uuidNameMap.TryGetValue(entity.UUID.ToString(), out string? mapped) &&
|
||||||
|
!string.IsNullOrWhiteSpace(mapped))
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Player";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(entity.Name))
|
||||||
|
return entity.Name;
|
||||||
|
|
||||||
|
return entity.Type.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BakeNameLabels(SampleResult result, List<EntityLabel> labels,
|
||||||
|
int mapW, int mapH)
|
||||||
|
{
|
||||||
|
if (labels.Count == 0) return;
|
||||||
|
int cellRows = mapH / 2;
|
||||||
|
|
||||||
|
var occupied = new HashSet<(int col, int row)>();
|
||||||
|
|
||||||
|
labels.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
int pa = MinimapEntityClassifier.GetPriority(
|
||||||
|
a.LabelColor == MinimapEntityClassifier.PlayerColor ? MobCategory.Player :
|
||||||
|
a.LabelColor == MinimapEntityClassifier.HostileColor ? MobCategory.Hostile :
|
||||||
|
a.LabelColor == MinimapEntityClassifier.NeutralColor ? MobCategory.Neutral : MobCategory.Passive);
|
||||||
|
int pb = MinimapEntityClassifier.GetPriority(
|
||||||
|
b.LabelColor == MinimapEntityClassifier.PlayerColor ? MobCategory.Player :
|
||||||
|
b.LabelColor == MinimapEntityClassifier.HostileColor ? MobCategory.Hostile :
|
||||||
|
b.LabelColor == MinimapEntityClassifier.NeutralColor ? MobCategory.Neutral : MobCategory.Passive);
|
||||||
|
return pb.CompareTo(pa);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var lbl in labels)
|
||||||
|
{
|
||||||
|
int cellRow = (lbl.PixelY / 2) + 1;
|
||||||
|
if (cellRow >= cellRows) cellRow = lbl.PixelY / 2 - 1;
|
||||||
|
if (cellRow < 0 || cellRow >= cellRows) continue;
|
||||||
|
|
||||||
|
int startCol = lbl.PixelX - lbl.Name.Length / 2;
|
||||||
|
startCol = Math.Clamp(startCol, 0, mapW - 1);
|
||||||
|
|
||||||
|
bool fits = true;
|
||||||
|
int endCol = Math.Min(startCol + lbl.Name.Length, mapW);
|
||||||
|
for (int c = startCol; c < endCol; c++)
|
||||||
|
{
|
||||||
|
if (occupied.Contains((c, cellRow)))
|
||||||
|
{
|
||||||
|
fits = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fits) continue;
|
||||||
|
|
||||||
|
for (int i = 0; i < lbl.Name.Length && startCol + i < mapW; i++)
|
||||||
|
{
|
||||||
|
int col = startCol + i;
|
||||||
|
occupied.Add((col, cellRow));
|
||||||
|
|
||||||
|
var bgTop = result.Pixels[col, cellRow * 2];
|
||||||
|
var bgBot = (cellRow * 2 + 1 < mapH)
|
||||||
|
? result.Pixels[col, cellRow * 2 + 1]
|
||||||
|
: bgTop;
|
||||||
|
|
||||||
|
var avgBg = Color.FromRgb(
|
||||||
|
(byte)((bgTop.R + bgBot.R) / 2),
|
||||||
|
(byte)((bgTop.G + bgBot.G) / 2),
|
||||||
|
(byte)((bgTop.B + bgBot.B) / 2));
|
||||||
|
|
||||||
|
result.CharOverlay[col, cellRow] = (lbl.Name[i], lbl.LabelColor, avgBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Color color, int surfaceY) SampleColumn(World world, int x, int z,
|
||||||
|
int scanTop, int minY,
|
||||||
|
ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ)
|
||||||
|
{
|
||||||
|
int chunkX = x >> 4;
|
||||||
|
int chunkZ = z >> 4;
|
||||||
|
if (chunkX != cachedChunkX || chunkZ != cachedChunkZ)
|
||||||
|
{
|
||||||
|
cachedColumn = world[chunkX, chunkZ];
|
||||||
|
cachedChunkX = chunkX;
|
||||||
|
cachedChunkZ = chunkZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedColumn is null)
|
||||||
|
return (MinimapColorMap.VoidColor, minY);
|
||||||
|
|
||||||
|
int waterDepth = 0;
|
||||||
|
bool inIce = false;
|
||||||
|
int surfaceY = minY;
|
||||||
|
|
||||||
|
for (int y = scanTop; y >= minY; y--)
|
||||||
|
{
|
||||||
|
var loc = new Mapping.Location(x, y, z);
|
||||||
|
var chunk = cachedColumn.GetChunk(loc);
|
||||||
|
if (chunk is null) continue;
|
||||||
|
|
||||||
|
var block = chunk.GetBlock(loc);
|
||||||
|
var mat = block.Type;
|
||||||
|
|
||||||
|
if (MinimapColorMap.IsFullyTransparent(mat))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (MinimapColorMap.IsWater(mat))
|
||||||
|
{
|
||||||
|
if (waterDepth == 0) surfaceY = y;
|
||||||
|
waterDepth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MinimapColorMap.IsIce(mat) && !inIce)
|
||||||
|
{
|
||||||
|
if (waterDepth == 0) surfaceY = y;
|
||||||
|
inIce = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waterDepth == 0 && !inIce) surfaceY = y;
|
||||||
|
|
||||||
|
var baseColor = MinimapColorMap.GetBaseColor(mat);
|
||||||
|
|
||||||
|
if (waterDepth > 0)
|
||||||
|
baseColor = MinimapColorMap.BlendWaterColor(baseColor, waterDepth);
|
||||||
|
if (inIce)
|
||||||
|
baseColor = MinimapColorMap.BlendIceColor(baseColor);
|
||||||
|
|
||||||
|
return (baseColor, surfaceY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waterDepth > 0)
|
||||||
|
return (MinimapColorMap.WaterColor, surfaceY);
|
||||||
|
|
||||||
|
return (MinimapColorMap.VoidColor, minY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Color color, int surfaceY) SampleAreaDominant(World world, int baseX, int baseZ,
|
||||||
|
int size, int scanTop, int minY,
|
||||||
|
ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ)
|
||||||
|
{
|
||||||
|
var colorCounts = new Dictionary<Color, (int Count, int SumY)>();
|
||||||
|
|
||||||
|
int step = Math.Max(1, size / 3);
|
||||||
|
for (int dx = 0; dx < size; dx += step)
|
||||||
|
{
|
||||||
|
for (int dz = 0; dz < size; dz += step)
|
||||||
|
{
|
||||||
|
var (c, surfY) = SampleColumn(world, baseX + dx, baseZ + dz, scanTop, minY,
|
||||||
|
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||||
|
|
||||||
|
if (colorCounts.TryGetValue(c, out var existing))
|
||||||
|
colorCounts[c] = (existing.Count + 1, existing.SumY + surfY);
|
||||||
|
else
|
||||||
|
colorCounts[c] = (1, surfY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color best = MinimapColorMap.VoidColor;
|
||||||
|
int bestCount = 0;
|
||||||
|
int avgY = minY;
|
||||||
|
foreach (var kvp in colorCounts)
|
||||||
|
{
|
||||||
|
if (kvp.Value.Count > bestCount)
|
||||||
|
{
|
||||||
|
bestCount = kvp.Value.Count;
|
||||||
|
best = kvp.Key;
|
||||||
|
avgY = kvp.Value.SumY / kvp.Value.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (best, avgY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyPixelBuffer(SampleResult result, int w, int h)
|
||||||
|
{
|
||||||
|
int rows = h / 2;
|
||||||
|
for (int row = 0; row < rows && row < _cellRows; row++)
|
||||||
|
{
|
||||||
|
for (int col = 0; col < w && col < _mapWidth; col++)
|
||||||
|
{
|
||||||
|
var overlay = result.CharOverlay[col, row];
|
||||||
|
if (overlay is not null)
|
||||||
|
{
|
||||||
|
var (ch, fg, bg) = overlay.Value;
|
||||||
|
_cells[row, col].Text = ch.ToString();
|
||||||
|
_cells[row, col].Foreground = new SolidColorBrush(fg);
|
||||||
|
_cells[row, col].Background = new SolidColorBrush(bg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var topColor = result.Pixels[col, row * 2];
|
||||||
|
var bottomColor = result.Pixels[col, row * 2 + 1];
|
||||||
|
|
||||||
|
_cells[row, col].Text = "\u2580";
|
||||||
|
_cells[row, col].Foreground = new SolidColorBrush(topColor);
|
||||||
|
_cells[row, col].Background = new SolidColorBrush(bottomColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateInfoBarAndLegend(McClient client, int bpp,
|
||||||
|
HashSet<MobCategory> categories, int mapW)
|
||||||
|
{
|
||||||
|
var loc = client.GetCurrentLocation();
|
||||||
|
float yaw = client.GetYaw();
|
||||||
|
string arrow = GetDirectionArrow(yaw);
|
||||||
|
|
||||||
|
int x = (int)Math.Floor(loc.X);
|
||||||
|
int y = (int)Math.Floor(loc.Y);
|
||||||
|
int z = (int)Math.Floor(loc.Z);
|
||||||
|
|
||||||
|
string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1";
|
||||||
|
|
||||||
|
var legendParts = new List<string>();
|
||||||
|
var legendColors = new List<Color>();
|
||||||
|
|
||||||
|
var sorted = categories
|
||||||
|
.Where(c => c != MobCategory.NonLiving)
|
||||||
|
.OrderByDescending(MinimapEntityClassifier.GetPriority);
|
||||||
|
|
||||||
|
int catCount = 0;
|
||||||
|
foreach (var cat in sorted)
|
||||||
|
{
|
||||||
|
if (catCount >= 4) break;
|
||||||
|
legendParts.Add(MinimapEntityClassifier.GetCategoryLabel(cat));
|
||||||
|
legendColors.Add(MinimapEntityClassifier.GetBaseColor(cat));
|
||||||
|
catCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int legendLen = 0;
|
||||||
|
for (int i = 0; i < legendParts.Count; i++)
|
||||||
|
legendLen += 1 + legendParts[i].Length + (i > 0 ? 1 : 0);
|
||||||
|
|
||||||
|
bool fitsOnOneLine = legendParts.Count > 0
|
||||||
|
&& coordPart.Length + 2 + legendLen <= mapW;
|
||||||
|
|
||||||
|
_infoRow.Children.Clear();
|
||||||
|
_infoRow.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = coordPart,
|
||||||
|
Foreground = Brushes.Gray,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fitsOnOneLine)
|
||||||
|
{
|
||||||
|
AppendLegendItems(_infoRow, legendParts, legendColors, leftMargin: 2);
|
||||||
|
_legendPanel.Children.Clear();
|
||||||
|
_legendPanel.IsVisible = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_legendPanel.IsVisible = legendParts.Count > 0;
|
||||||
|
_legendPanel.Children.Clear();
|
||||||
|
AppendLegendItems(_legendPanel, legendParts, legendColors, leftMargin: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendLegendItems(StackPanel panel,
|
||||||
|
List<string> parts, List<Color> colors, int leftMargin)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
int ml = i == 0 ? leftMargin : 1;
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\u25cf",
|
||||||
|
Foreground = new SolidColorBrush(colors[i]),
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = ml > 0 ? new Thickness(ml, 0, 0, 0) : new Thickness(0),
|
||||||
|
});
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = parts[i],
|
||||||
|
Foreground = Brushes.Gray,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = new Thickness(0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDirectionArrow(float yaw)
|
||||||
|
{
|
||||||
|
double normalized = ((yaw % 360) + 360) % 360;
|
||||||
|
int index = (int)Math.Round(normalized / 45.0) % 8;
|
||||||
|
return index switch
|
||||||
|
{
|
||||||
|
0 => "\u2193", // S
|
||||||
|
1 => "\u2199", // SW
|
||||||
|
2 => "\u2190", // W
|
||||||
|
3 => "\u2196", // NW
|
||||||
|
4 => "\u2191", // N
|
||||||
|
5 => "\u2197", // NE
|
||||||
|
6 => "\u2192", // E
|
||||||
|
7 => "\u2198", // SE
|
||||||
|
_ => "\u2193",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
MinecraftClient/Tui/MinimapEntityCategories.json
Normal file
167
MinecraftClient/Tui/MinimapEntityCategories.json
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
{
|
||||||
|
"version": "26.1-rc-2",
|
||||||
|
"hostile": [
|
||||||
|
"Blaze",
|
||||||
|
"Bogged",
|
||||||
|
"Breeze",
|
||||||
|
"CamelHusk",
|
||||||
|
"Creaking",
|
||||||
|
"Creeper",
|
||||||
|
"Drowned",
|
||||||
|
"ElderGuardian",
|
||||||
|
"EnderDragon",
|
||||||
|
"Endermite",
|
||||||
|
"Evoker",
|
||||||
|
"Ghast",
|
||||||
|
"Giant",
|
||||||
|
"Guardian",
|
||||||
|
"Hoglin",
|
||||||
|
"Husk",
|
||||||
|
"Illusioner",
|
||||||
|
"MagmaCube",
|
||||||
|
"Parched",
|
||||||
|
"Phantom",
|
||||||
|
"Piglin",
|
||||||
|
"PiglinBrute",
|
||||||
|
"Pillager",
|
||||||
|
"Ravager",
|
||||||
|
"Shulker",
|
||||||
|
"Silverfish",
|
||||||
|
"Skeleton",
|
||||||
|
"Slime",
|
||||||
|
"Stray",
|
||||||
|
"Vex",
|
||||||
|
"Vindicator",
|
||||||
|
"Warden",
|
||||||
|
"Witch",
|
||||||
|
"Wither",
|
||||||
|
"WitherSkeleton",
|
||||||
|
"Zoglin",
|
||||||
|
"Zombie",
|
||||||
|
"ZombieNautilus",
|
||||||
|
"ZombieVillager"
|
||||||
|
],
|
||||||
|
"passive": [
|
||||||
|
"Allay",
|
||||||
|
"Armadillo",
|
||||||
|
"Axolotl",
|
||||||
|
"Bat",
|
||||||
|
"Camel",
|
||||||
|
"Cat",
|
||||||
|
"Chicken",
|
||||||
|
"Cod",
|
||||||
|
"Cow",
|
||||||
|
"Donkey",
|
||||||
|
"Fox",
|
||||||
|
"Frog",
|
||||||
|
"GlowSquid",
|
||||||
|
"HappyGhast",
|
||||||
|
"Horse",
|
||||||
|
"Mooshroom",
|
||||||
|
"Mule",
|
||||||
|
"Nautilus",
|
||||||
|
"Ocelot",
|
||||||
|
"Parrot",
|
||||||
|
"Pig",
|
||||||
|
"Pufferfish",
|
||||||
|
"Rabbit",
|
||||||
|
"Salmon",
|
||||||
|
"Sheep",
|
||||||
|
"SkeletonHorse",
|
||||||
|
"Sniffer",
|
||||||
|
"Squid",
|
||||||
|
"Strider",
|
||||||
|
"Tadpole",
|
||||||
|
"TropicalFish",
|
||||||
|
"Turtle",
|
||||||
|
"Villager",
|
||||||
|
"WanderingTrader",
|
||||||
|
"ZombieHorse"
|
||||||
|
],
|
||||||
|
"neutral": [
|
||||||
|
"Bee",
|
||||||
|
"CaveSpider",
|
||||||
|
"CopperGolem",
|
||||||
|
"Dolphin",
|
||||||
|
"Enderman",
|
||||||
|
"Goat",
|
||||||
|
"IronGolem",
|
||||||
|
"Llama",
|
||||||
|
"Panda",
|
||||||
|
"PolarBear",
|
||||||
|
"SnowGolem",
|
||||||
|
"Spider",
|
||||||
|
"TraderLlama",
|
||||||
|
"Wolf",
|
||||||
|
"ZombifiedPiglin"
|
||||||
|
],
|
||||||
|
"non_living": [
|
||||||
|
"AcaciaBoat",
|
||||||
|
"AcaciaChestBoat",
|
||||||
|
"AreaEffectCloud",
|
||||||
|
"ArmorStand",
|
||||||
|
"Arrow",
|
||||||
|
"BambooChestRaft",
|
||||||
|
"BambooRaft",
|
||||||
|
"BirchBoat",
|
||||||
|
"BirchChestBoat",
|
||||||
|
"BlockDisplay",
|
||||||
|
"BreezeWindCharge",
|
||||||
|
"CherryBoat",
|
||||||
|
"CherryChestBoat",
|
||||||
|
"ChestMinecart",
|
||||||
|
"CommandBlockMinecart",
|
||||||
|
"DarkOakBoat",
|
||||||
|
"DarkOakChestBoat",
|
||||||
|
"DragonFireball",
|
||||||
|
"Egg",
|
||||||
|
"EndCrystal",
|
||||||
|
"EnderPearl",
|
||||||
|
"EvokerFangs",
|
||||||
|
"ExperienceBottle",
|
||||||
|
"ExperienceOrb",
|
||||||
|
"EyeOfEnder",
|
||||||
|
"FallingBlock",
|
||||||
|
"Fireball",
|
||||||
|
"FireworkRocket",
|
||||||
|
"FishingBobber",
|
||||||
|
"FurnaceMinecart",
|
||||||
|
"GlowItemFrame",
|
||||||
|
"HopperMinecart",
|
||||||
|
"Interaction",
|
||||||
|
"Item",
|
||||||
|
"ItemDisplay",
|
||||||
|
"ItemFrame",
|
||||||
|
"JungleBoat",
|
||||||
|
"JungleChestBoat",
|
||||||
|
"LeashKnot",
|
||||||
|
"LightningBolt",
|
||||||
|
"LingeringPotion",
|
||||||
|
"LlamaSpit",
|
||||||
|
"MangroveBoat",
|
||||||
|
"MangroveChestBoat",
|
||||||
|
"Mannequin",
|
||||||
|
"Marker",
|
||||||
|
"Minecart",
|
||||||
|
"OakBoat",
|
||||||
|
"OakChestBoat",
|
||||||
|
"OminousItemSpawner",
|
||||||
|
"Painting",
|
||||||
|
"PaleOakBoat",
|
||||||
|
"PaleOakChestBoat",
|
||||||
|
"ShulkerBullet",
|
||||||
|
"SmallFireball",
|
||||||
|
"Snowball",
|
||||||
|
"SpawnerMinecart",
|
||||||
|
"SpectralArrow",
|
||||||
|
"SplashPotion",
|
||||||
|
"SpruceBoat",
|
||||||
|
"SpruceChestBoat",
|
||||||
|
"TextDisplay",
|
||||||
|
"Tnt",
|
||||||
|
"TntMinecart",
|
||||||
|
"Trident",
|
||||||
|
"WindCharge",
|
||||||
|
"WitherSkull"
|
||||||
|
]
|
||||||
|
}
|
||||||
178
MinecraftClient/Tui/MinimapEntityClassifier.cs
Normal file
178
MinecraftClient/Tui/MinimapEntityClassifier.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using MinecraftClient.Mapping;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tui
|
||||||
|
{
|
||||||
|
public enum MobCategory
|
||||||
|
{
|
||||||
|
Hostile,
|
||||||
|
Passive,
|
||||||
|
Neutral,
|
||||||
|
Player,
|
||||||
|
NonLiving,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MinimapPosition
|
||||||
|
{
|
||||||
|
top_left,
|
||||||
|
top_right,
|
||||||
|
center,
|
||||||
|
bottom_left,
|
||||||
|
bottom_right,
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NameDisplayConfig
|
||||||
|
{
|
||||||
|
public volatile bool Players = false;
|
||||||
|
public volatile bool Hostile = false;
|
||||||
|
public volatile bool Neutral = false;
|
||||||
|
public volatile bool Passive = false;
|
||||||
|
|
||||||
|
public bool AnyEnabled => Players || Hostile || Neutral || Passive;
|
||||||
|
|
||||||
|
public void SetAll(bool value)
|
||||||
|
{
|
||||||
|
Players = value;
|
||||||
|
Hostile = value;
|
||||||
|
Neutral = value;
|
||||||
|
Passive = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldShowName(MobCategory category) => category switch
|
||||||
|
{
|
||||||
|
MobCategory.Player => Players,
|
||||||
|
MobCategory.Hostile => Hostile,
|
||||||
|
MobCategory.Neutral => Neutral,
|
||||||
|
MobCategory.Passive => Passive,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Classifies entities into minimap categories using data extracted from
|
||||||
|
/// Minecraft's MobCategory assignments. Categories are loaded from the
|
||||||
|
/// embedded MinimapEntityCategories.json resource generated by
|
||||||
|
/// tools/gen_entity_category_map.py.
|
||||||
|
/// </summary>
|
||||||
|
public static class MinimapEntityClassifier
|
||||||
|
{
|
||||||
|
public static readonly Color HostileColor = Color.FromRgb(255, 68, 68);
|
||||||
|
public static readonly Color PassiveColor = Color.FromRgb(68, 255, 68);
|
||||||
|
public static readonly Color NeutralColor = Color.FromRgb(255, 170, 0);
|
||||||
|
public static readonly Color PlayerColor = Color.FromRgb(255, 255, 255);
|
||||||
|
public static readonly Color FadedGray = Color.FromRgb(100, 100, 100);
|
||||||
|
|
||||||
|
private static readonly FrozenDictionary<EntityType, MobCategory> CategoryTable;
|
||||||
|
|
||||||
|
static MinimapEntityClassifier()
|
||||||
|
{
|
||||||
|
var table = new Dictionary<EntityType, MobCategory>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceStream("MinimapEntityCategories.json");
|
||||||
|
if (stream is not null)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(stream);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
LoadCategory(root, "hostile", MobCategory.Hostile, table);
|
||||||
|
LoadCategory(root, "passive", MobCategory.Passive, table);
|
||||||
|
LoadCategory(root, "neutral", MobCategory.Neutral, table);
|
||||||
|
LoadCategory(root, "non_living", MobCategory.NonLiving, table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleIO.WriteLogLine($"[Minimap] Failed to load entity categories: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryTable = table.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LoadCategory(JsonElement root, string key,
|
||||||
|
MobCategory category, Dictionary<EntityType, MobCategory> table)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty(key, out var arr))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var el in arr.EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = el.GetString();
|
||||||
|
if (name is not null && Enum.TryParse<EntityType>(name, out var et))
|
||||||
|
table.TryAdd(et, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobCategory Classify(EntityType type)
|
||||||
|
{
|
||||||
|
if (type == EntityType.Player)
|
||||||
|
return MobCategory.Player;
|
||||||
|
return CategoryTable.GetValueOrDefault(type, MobCategory.NonLiving);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color GetBaseColor(MobCategory category) => category switch
|
||||||
|
{
|
||||||
|
MobCategory.Hostile => HostileColor,
|
||||||
|
MobCategory.Passive => PassiveColor,
|
||||||
|
MobCategory.Neutral => NeutralColor,
|
||||||
|
MobCategory.Player => PlayerColor,
|
||||||
|
_ => FadedGray,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Color ApplyDepthFade(Color baseColor, double playerY, double entityY)
|
||||||
|
{
|
||||||
|
double depth = playerY - entityY;
|
||||||
|
|
||||||
|
if (depth <= 5.0)
|
||||||
|
return baseColor;
|
||||||
|
|
||||||
|
if (depth >= 15.0)
|
||||||
|
return FadedGray;
|
||||||
|
|
||||||
|
double t = (depth - 5.0) / 10.0;
|
||||||
|
return Lerp(baseColor, FadedGray, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ShouldDisplay(MobCategory category, double playerY, double entityY)
|
||||||
|
{
|
||||||
|
if (category == MobCategory.Player)
|
||||||
|
return true;
|
||||||
|
if (entityY >= playerY)
|
||||||
|
return true;
|
||||||
|
return playerY - entityY <= 15.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetPriority(MobCategory category) => category switch
|
||||||
|
{
|
||||||
|
MobCategory.Hostile => 4,
|
||||||
|
MobCategory.Player => 3,
|
||||||
|
MobCategory.Neutral => 2,
|
||||||
|
MobCategory.Passive => 1,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetCategoryLabel(MobCategory category) => category switch
|
||||||
|
{
|
||||||
|
MobCategory.Hostile => Translations.tui_minimap_legend_hostile,
|
||||||
|
MobCategory.Passive => Translations.tui_minimap_legend_passive,
|
||||||
|
MobCategory.Neutral => Translations.tui_minimap_legend_neutral,
|
||||||
|
MobCategory.Player => Translations.tui_minimap_legend_player,
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color Lerp(Color a, Color b, double t)
|
||||||
|
{
|
||||||
|
byte r = (byte)(a.R + (b.R - a.R) * t);
|
||||||
|
byte g = (byte)(a.G + (b.G - a.G) * t);
|
||||||
|
byte bl = (byte)(a.B + (b.B - a.B) * t);
|
||||||
|
return Color.FromRgb(r, g, bl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -135,6 +135,44 @@ Data source: `https://raw.githubusercontent.com/PrismarineJS/minecraft-data/mast
|
||||||
|
|
||||||
Uses `curl` with resume (`-C -`) for reliable download over slow connections. Falls back to manual download if retries are exhausted.
|
Uses `curl` with resume (`-C -`) for reliable download over slow connections. Falls back to manual download if retries are exhausted.
|
||||||
|
|
||||||
|
## gen_block_color_map.py -- Generate minimap block color JSON
|
||||||
|
|
||||||
|
Extracts block-to-MapColor RGB mappings from decompiled Minecraft source for the TUI minimap.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||||
|
# -> MinecraftClient/Tui/MinimapBlockColors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses three files from the decompiled source:
|
||||||
|
- `MapColor.java` -- extracts the 64 base MapColor constants and their RGB values
|
||||||
|
- `DyeColor.java` -- maps dye colors to MapColor constants
|
||||||
|
- `Blocks.java` -- determines each block's assigned MapColor via `.mapColor()` calls
|
||||||
|
|
||||||
|
Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`). Contains color entries, plus lists of transparent, water, and ice materials.
|
||||||
|
|
||||||
|
Validates each block name against MCC's `Material.cs` enum. Blocks without a matching enum value are skipped.
|
||||||
|
|
||||||
|
## gen_entity_category_map.py -- Generate minimap entity category JSON
|
||||||
|
|
||||||
|
Extracts entity-to-MobCategory mappings from decompiled Minecraft source for the TUI minimap.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||||
|
# -> MinecraftClient/Tui/MinimapEntityCategories.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses `EntityType.java` to read each entity's `MobCategory` assignment from the `EntityType.Builder.of(Factory, MobCategory.XXX)` call. Maps Minecraft categories to MCC minimap categories:
|
||||||
|
- `MONSTER` -> hostile
|
||||||
|
- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive
|
||||||
|
- `MISC` -> non_living
|
||||||
|
|
||||||
|
The script maintains manual override lists for:
|
||||||
|
- **Neutral mobs** (e.g. Enderman, Spider, Wolf, Bee) -- Minecraft has no "neutral" category; these are MONSTER or CREATURE in code but only attack when provoked
|
||||||
|
- **Passive overrides** (e.g. Villager, WanderingTrader) -- classified as MISC in Minecraft for spawning reasons but should appear as passive on the minimap
|
||||||
|
|
||||||
|
Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`). Validates each entity name against MCC's `EntityType.cs` enum.
|
||||||
|
|
||||||
## Recommended workflow
|
## Recommended workflow
|
||||||
|
|
||||||
1. Generate server reports (Step 0)
|
1. Generate server reports (Step 0)
|
||||||
|
|
@ -145,6 +183,9 @@ Uses `curl` with resume (`-C -`) for reliable download over slow connections. Fa
|
||||||
- Entities: `gen_entity_palette.py`
|
- Entities: `gen_entity_palette.py`
|
||||||
- Metadata: `gen_entity_metadata_palette.py`
|
- Metadata: `gen_entity_metadata_palette.py`
|
||||||
4. Update block collision shapes: `gen_block_shapes.py`
|
4. Update block collision shapes: `gen_block_shapes.py`
|
||||||
5. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs`
|
5. Update minimap data (if blocks or entities changed):
|
||||||
6. Update version routing (see SKILL.md)
|
- Block colors: `gen_block_color_map.py`
|
||||||
7. Build and test
|
- Entity categories: `gen_entity_category_map.py`
|
||||||
|
6. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs`
|
||||||
|
7. Update version routing (see SKILL.md)
|
||||||
|
8. Build and test
|
||||||
|
|
|
||||||
268
tools/gen_block_color_map.py
Normal file
268
tools/gen_block_color_map.py
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate MinimapBlockColors.json from decompiled Minecraft source.
|
||||||
|
|
||||||
|
Parses MapColor.java for the 62 base map colors (ID -> RGB), then parses
|
||||||
|
Blocks.java to extract each block's mapColor assignment, and outputs a
|
||||||
|
JSON mapping from MCC Material enum names (PascalCase) to RGB triples.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/gen_block_color_map.py <decompiled_root>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUTPUT_PATH = (Path(__file__).resolve().parent.parent
|
||||||
|
/ "MinecraftClient" / "Tui" / "MinimapBlockColors.json")
|
||||||
|
MATERIAL_CS = (Path(__file__).resolve().parent.parent
|
||||||
|
/ "MinecraftClient" / "Mapping" / "Material.cs")
|
||||||
|
|
||||||
|
|
||||||
|
def mc_name_to_csharp(mc_name: str) -> str:
|
||||||
|
name = mc_name.removeprefix("minecraft:")
|
||||||
|
return "".join(word.capitalize() for word in name.split("_"))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_map_colors(map_color_java: Path) -> dict[str, tuple[int, int, int]]:
|
||||||
|
"""Parse MapColor.java: extract name -> (R, G, B) for each constant."""
|
||||||
|
text = map_color_java.read_text()
|
||||||
|
colors: dict[str, tuple[int, int, int]] = {}
|
||||||
|
|
||||||
|
pattern = re.compile(
|
||||||
|
r'public static final MapColor\s+(\w+)\s*=\s*new\s+MapColor\(\s*(\d+)\s*,\s*(\d+)\s*\)')
|
||||||
|
for m in pattern.finditer(text):
|
||||||
|
name = m.group(1)
|
||||||
|
color_int = int(m.group(3))
|
||||||
|
r = (color_int >> 16) & 0xFF
|
||||||
|
g = (color_int >> 8) & 0xFF
|
||||||
|
b = color_int & 0xFF
|
||||||
|
colors[name] = (r, g, b)
|
||||||
|
|
||||||
|
return colors
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dye_to_map_color(dye_color_java: Path) -> dict[str, str]:
|
||||||
|
"""Parse DyeColor.java: extract DyeColor name -> MapColor name."""
|
||||||
|
text = dye_color_java.read_text()
|
||||||
|
mapping: dict[str, str] = {}
|
||||||
|
|
||||||
|
pattern = re.compile(
|
||||||
|
r'(\w+)\(\d+,\s*"[^"]+",\s*\d+,\s*MapColor\.(\w+)')
|
||||||
|
for m in pattern.finditer(text):
|
||||||
|
mapping[m.group(1)] = m.group(2)
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def extract_block_declarations(text: str) -> list[tuple[str, str, str]]:
|
||||||
|
"""Extract (field_name, block_id, full_register_body) for each block declaration.
|
||||||
|
|
||||||
|
Returns list of (FIELD_NAME, "block_name", "register(...) content").
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Find all "public static final Block FIELD = register(...)" declarations.
|
||||||
|
# These span multiple lines and end with ");".
|
||||||
|
# Strategy: find start pattern, then track parens to find matching end.
|
||||||
|
field_pattern = re.compile(
|
||||||
|
r'public\s+static\s+final\s+Block\s+(\w+)\s*=\s*register\s*\(')
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
while pos < len(text):
|
||||||
|
m = field_pattern.search(text, pos)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
field_name = m.group(1)
|
||||||
|
paren_start = m.end() - 1 # position of opening '('
|
||||||
|
|
||||||
|
# Find matching closing ')' then ';'
|
||||||
|
depth = 1
|
||||||
|
i = paren_start + 1
|
||||||
|
while i < len(text) and depth > 0:
|
||||||
|
if text[i] == '(':
|
||||||
|
depth += 1
|
||||||
|
elif text[i] == ')':
|
||||||
|
depth -= 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
register_body = text[paren_start:i]
|
||||||
|
|
||||||
|
# Extract block name string from register call
|
||||||
|
name_match = re.search(r'(?:BlockIds\.(\w+)|"(\w+)")', register_body)
|
||||||
|
if name_match:
|
||||||
|
raw_id = name_match.group(1) or name_match.group(2)
|
||||||
|
block_id = raw_id.lower() if raw_id.isupper() else raw_id
|
||||||
|
else:
|
||||||
|
block_id = field_name.lower()
|
||||||
|
|
||||||
|
results.append((field_name, block_id, register_body))
|
||||||
|
pos = i
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_blocks(blocks_java: Path, map_colors: dict[str, tuple[int, int, int]],
|
||||||
|
dye_to_map: dict[str, str]) -> dict[str, tuple[int, int, int]]:
|
||||||
|
"""Parse Blocks.java: extract block_name -> (R, G, B)."""
|
||||||
|
text = blocks_java.read_text()
|
||||||
|
|
||||||
|
declarations = extract_block_declarations(text)
|
||||||
|
print(f" Found {len(declarations)} block register() declarations")
|
||||||
|
|
||||||
|
# First pass: assign MapColor name to each block
|
||||||
|
field_to_block_id: dict[str, str] = {}
|
||||||
|
block_color_name: dict[str, str] = {}
|
||||||
|
|
||||||
|
map_color_direct = re.compile(r'\.mapColor\(MapColor\.(\w+)\)')
|
||||||
|
map_color_dye = re.compile(r'\.mapColor\(DyeColor\.(\w+)\)')
|
||||||
|
map_color_ref = re.compile(r'\.mapColor\((\w+)\.defaultMapColor\(\)')
|
||||||
|
map_color_waterlogged = re.compile(r'\.mapColor\(waterloggedMapColor\(MapColor\.(\w+)\)')
|
||||||
|
|
||||||
|
for field_name, block_id, body in declarations:
|
||||||
|
field_to_block_id[field_name] = block_id
|
||||||
|
|
||||||
|
mc = map_color_direct.search(body)
|
||||||
|
if mc:
|
||||||
|
block_color_name[block_id] = mc.group(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
mc = map_color_dye.search(body)
|
||||||
|
if mc:
|
||||||
|
dye_name = mc.group(1)
|
||||||
|
if dye_name in dye_to_map:
|
||||||
|
block_color_name[block_id] = dye_to_map[dye_name]
|
||||||
|
continue
|
||||||
|
|
||||||
|
mc = map_color_waterlogged.search(body)
|
||||||
|
if mc:
|
||||||
|
block_color_name[block_id] = mc.group(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
mc = map_color_ref.search(body)
|
||||||
|
if mc:
|
||||||
|
ref_field = mc.group(1)
|
||||||
|
ref_block = field_to_block_id.get(ref_field)
|
||||||
|
if ref_block and ref_block in block_color_name:
|
||||||
|
block_color_name[block_id] = block_color_name[ref_block]
|
||||||
|
|
||||||
|
# Second pass: resolve remaining BLOCK.defaultMapColor() references
|
||||||
|
for field_name, block_id, body in declarations:
|
||||||
|
if block_id in block_color_name:
|
||||||
|
continue
|
||||||
|
mc = map_color_ref.search(body)
|
||||||
|
if mc:
|
||||||
|
ref_field = mc.group(1)
|
||||||
|
ref_block = field_to_block_id.get(ref_field)
|
||||||
|
if ref_block and ref_block in block_color_name:
|
||||||
|
block_color_name[block_id] = block_color_name[ref_block]
|
||||||
|
|
||||||
|
result: dict[str, tuple[int, int, int]] = {}
|
||||||
|
for block_id, color_name in block_color_name.items():
|
||||||
|
if color_name in map_colors:
|
||||||
|
cs_name = mc_name_to_csharp(block_id)
|
||||||
|
result[cs_name] = map_colors[color_name]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_known_materials() -> set[str]:
|
||||||
|
known = set()
|
||||||
|
if MATERIAL_CS.exists():
|
||||||
|
with open(MATERIAL_CS) as f:
|
||||||
|
for line in f:
|
||||||
|
m = re.match(r'\s+(\w+),?\s*$', line)
|
||||||
|
if m:
|
||||||
|
known.add(m.group(1))
|
||||||
|
return known
|
||||||
|
|
||||||
|
|
||||||
|
TRANSPARENT_BLOCKS = [
|
||||||
|
"Air", "CaveAir", "VoidAir",
|
||||||
|
"Glass", "GlassPane",
|
||||||
|
"WhiteStainedGlass", "OrangeStainedGlass", "MagentaStainedGlass",
|
||||||
|
"LightBlueStainedGlass", "YellowStainedGlass", "LimeStainedGlass",
|
||||||
|
"PinkStainedGlass", "GrayStainedGlass", "LightGrayStainedGlass",
|
||||||
|
"CyanStainedGlass", "PurpleStainedGlass", "BlueStainedGlass",
|
||||||
|
"BrownStainedGlass", "GreenStainedGlass", "RedStainedGlass",
|
||||||
|
"BlackStainedGlass",
|
||||||
|
"WhiteStainedGlassPane", "OrangeStainedGlassPane", "MagentaStainedGlassPane",
|
||||||
|
"LightBlueStainedGlassPane", "YellowStainedGlassPane", "LimeStainedGlassPane",
|
||||||
|
"PinkStainedGlassPane", "GrayStainedGlassPane", "LightGrayStainedGlassPane",
|
||||||
|
"CyanStainedGlassPane", "PurpleStainedGlassPane", "BlueStainedGlassPane",
|
||||||
|
"BrownStainedGlassPane", "GreenStainedGlassPane", "RedStainedGlassPane",
|
||||||
|
"BlackStainedGlassPane",
|
||||||
|
"TintedGlass", "Barrier", "Light", "StructureVoid",
|
||||||
|
]
|
||||||
|
|
||||||
|
WATER_BLOCKS = ["Water"]
|
||||||
|
ICE_BLOCKS = ["Ice", "PackedIce", "BlueIce", "FrostedIce"]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
root = Path(sys.argv[1])
|
||||||
|
if not root.is_dir():
|
||||||
|
print(f"Error: {root} is not a directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
map_color_java = root / "net/minecraft/world/level/material/MapColor.java"
|
||||||
|
dye_color_java = root / "net/minecraft/world/item/DyeColor.java"
|
||||||
|
blocks_java = root / "net/minecraft/world/level/block/Blocks.java"
|
||||||
|
|
||||||
|
for f in [map_color_java, dye_color_java, blocks_java]:
|
||||||
|
if not f.exists():
|
||||||
|
print(f"Error: {f} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Parsing MapColor.java...")
|
||||||
|
map_colors = parse_map_colors(map_color_java)
|
||||||
|
print(f" Found {len(map_colors)} map colors")
|
||||||
|
|
||||||
|
print("Parsing DyeColor.java...")
|
||||||
|
dye_to_map = parse_dye_to_map_color(dye_color_java)
|
||||||
|
print(f" Found {len(dye_to_map)} dye->map color mappings")
|
||||||
|
|
||||||
|
print("Parsing Blocks.java...")
|
||||||
|
block_colors = parse_blocks(blocks_java, map_colors, dye_to_map)
|
||||||
|
print(f" Extracted colors for {len(block_colors)} blocks")
|
||||||
|
|
||||||
|
known_materials = load_known_materials()
|
||||||
|
if known_materials:
|
||||||
|
matched = {k: v for k, v in block_colors.items() if k in known_materials}
|
||||||
|
unmatched = [k for k in block_colors if k not in known_materials]
|
||||||
|
if unmatched:
|
||||||
|
print(f"\n {len(unmatched)} blocks not in Material.cs (will be skipped):")
|
||||||
|
for name in sorted(unmatched)[:20]:
|
||||||
|
print(f" {name}")
|
||||||
|
if len(unmatched) > 20:
|
||||||
|
print(f" ... and {len(unmatched) - 20} more")
|
||||||
|
block_colors = matched
|
||||||
|
print(f" {len(block_colors)} blocks matched to Material.cs entries")
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"version": root.name.replace("-decompiled", "").replace("-client", ""),
|
||||||
|
"colors": {k: list(v) for k, v in sorted(block_colors.items())},
|
||||||
|
"transparent": sorted(TRANSPARENT_BLOCKS),
|
||||||
|
"water": WATER_BLOCKS,
|
||||||
|
"ice": ICE_BLOCKS,
|
||||||
|
}
|
||||||
|
|
||||||
|
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(OUTPUT_PATH, 'w') as f:
|
||||||
|
json.dump(output, f, indent=2)
|
||||||
|
print(f"\nGenerated {OUTPUT_PATH}")
|
||||||
|
print(f" {len(block_colors)} color entries")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
200
tools/gen_entity_category_map.py
Normal file
200
tools/gen_entity_category_map.py
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate MinimapEntityCategories.json from decompiled Minecraft source.
|
||||||
|
|
||||||
|
Parses EntityType.java to extract each entity's MobCategory assignment,
|
||||||
|
then maps them to MCC minimap categories (hostile/passive/neutral/non_living).
|
||||||
|
|
||||||
|
Minecraft's MobCategory values:
|
||||||
|
MONSTER -> hostile (with neutral overrides for conditionally hostile mobs)
|
||||||
|
CREATURE -> passive (with neutral overrides for conditionally hostile mobs)
|
||||||
|
AMBIENT -> passive
|
||||||
|
AXOLOTLS -> passive
|
||||||
|
WATER_CREATURE -> passive
|
||||||
|
WATER_AMBIENT -> passive
|
||||||
|
UNDERGROUND_WATER_CREATURE -> passive
|
||||||
|
MISC -> non_living
|
||||||
|
|
||||||
|
Some mobs classified as MONSTER or CREATURE are actually "neutral" -- they
|
||||||
|
only attack when provoked. These are listed in NEUTRAL_OVERRIDES below and
|
||||||
|
should be updated when new conditionally-hostile mobs are added.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/gen_entity_category_map.py <decompiled_root>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUTPUT_PATH = (Path(__file__).resolve().parent.parent
|
||||||
|
/ "MinecraftClient" / "Tui" / "MinimapEntityCategories.json")
|
||||||
|
ENTITY_TYPE_CS = (Path(__file__).resolve().parent.parent
|
||||||
|
/ "MinecraftClient" / "Mapping" / "EntityType.cs")
|
||||||
|
|
||||||
|
|
||||||
|
def mc_name_to_csharp(mc_name: str) -> str:
|
||||||
|
name = mc_name.removeprefix("minecraft:")
|
||||||
|
return "".join(word.capitalize() for word in name.split("_"))
|
||||||
|
|
||||||
|
|
||||||
|
# Mobs that Minecraft classifies as MONSTER or CREATURE but behave as
|
||||||
|
# "neutral" -- they only attack when provoked. This list is maintained
|
||||||
|
# manually because there is no machine-readable flag in the game data.
|
||||||
|
NEUTRAL_OVERRIDES = {
|
||||||
|
"bee", "dolphin", "goat", "iron_golem", "llama", "panda",
|
||||||
|
"polar_bear", "snow_golem", "trader_llama", "wolf",
|
||||||
|
"zombified_piglin", "enderman", "spider", "cave_spider",
|
||||||
|
"copper_golem",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Entities whose MobCategory in the game code doesn't match how they
|
||||||
|
# should appear on the minimap. For example, Villager and WanderingTrader
|
||||||
|
# are MISC in MC code (for spawning reasons) but should be passive on the map.
|
||||||
|
# ZombieHorse is MONSTER but is a rideable passive mob in practice.
|
||||||
|
PASSIVE_OVERRIDES = {
|
||||||
|
"villager", "wandering_trader", "zombie_horse",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Player has its own category in MCC -- extracted from MISC to "player".
|
||||||
|
PLAYER_OVERRIDES = {"player"}
|
||||||
|
|
||||||
|
MC_TO_MCC = {
|
||||||
|
"MONSTER": "hostile",
|
||||||
|
"CREATURE": "passive",
|
||||||
|
"AMBIENT": "passive",
|
||||||
|
"AXOLOTLS": "passive",
|
||||||
|
"WATER_CREATURE": "passive",
|
||||||
|
"WATER_AMBIENT": "passive",
|
||||||
|
"UNDERGROUND_WATER_CREATURE": "passive",
|
||||||
|
"MISC": "non_living",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_entity_categories(entity_type_java: Path) -> list[tuple[str, str, str]]:
|
||||||
|
"""Extract (entity_id, field_name, MobCategory) from EntityType.java.
|
||||||
|
|
||||||
|
Returns list of (entity_id, FIELD_NAME, MobCategory_name).
|
||||||
|
"""
|
||||||
|
text = entity_type_java.read_text()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
field_pat = re.compile(
|
||||||
|
r'public\s+static\s+final\s+EntityType<[^>]+>\s+(\w+)\s*=\s*register\s*\(')
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
while pos < len(text):
|
||||||
|
m = field_pat.search(text, pos)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
field_name = m.group(1)
|
||||||
|
paren_start = m.end() - 1
|
||||||
|
depth = 1
|
||||||
|
i = paren_start + 1
|
||||||
|
while i < len(text) and depth > 0:
|
||||||
|
if text[i] == '(':
|
||||||
|
depth += 1
|
||||||
|
elif text[i] == ')':
|
||||||
|
depth -= 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
body = text[paren_start:i]
|
||||||
|
|
||||||
|
name_match = re.search(r'"(\w+)"', body)
|
||||||
|
entity_id = name_match.group(1) if name_match else field_name.lower()
|
||||||
|
|
||||||
|
cat_match = re.search(r'MobCategory\.(\w+)', body)
|
||||||
|
mob_cat = cat_match.group(1) if cat_match else "MISC"
|
||||||
|
|
||||||
|
results.append((entity_id, field_name, mob_cat))
|
||||||
|
pos = i
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def load_known_entity_types() -> set[str]:
|
||||||
|
known = set()
|
||||||
|
if ENTITY_TYPE_CS.exists():
|
||||||
|
with open(ENTITY_TYPE_CS) as f:
|
||||||
|
for line in f:
|
||||||
|
m = re.match(r'\s+(\w+),?\s*$', line)
|
||||||
|
if m:
|
||||||
|
known.add(m.group(1))
|
||||||
|
return known
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
root = Path(sys.argv[1])
|
||||||
|
entity_type_java = root / "net/minecraft/world/entity/EntityType.java"
|
||||||
|
|
||||||
|
if not entity_type_java.exists():
|
||||||
|
print(f"Error: {entity_type_java} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Parsing EntityType.java...")
|
||||||
|
entities = extract_entity_categories(entity_type_java)
|
||||||
|
print(f" Found {len(entities)} entity type declarations")
|
||||||
|
|
||||||
|
known_types = load_known_entity_types()
|
||||||
|
|
||||||
|
hostile = []
|
||||||
|
passive = []
|
||||||
|
neutral = []
|
||||||
|
non_living = []
|
||||||
|
|
||||||
|
for entity_id, field_name, mob_cat in entities:
|
||||||
|
cs_name = mc_name_to_csharp(entity_id)
|
||||||
|
|
||||||
|
if known_types and cs_name not in known_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if entity_id in PLAYER_OVERRIDES:
|
||||||
|
continue
|
||||||
|
elif entity_id in NEUTRAL_OVERRIDES:
|
||||||
|
neutral.append(cs_name)
|
||||||
|
elif entity_id in PASSIVE_OVERRIDES:
|
||||||
|
passive.append(cs_name)
|
||||||
|
elif mob_cat in MC_TO_MCC:
|
||||||
|
cat = MC_TO_MCC[mob_cat]
|
||||||
|
if cat == "hostile":
|
||||||
|
hostile.append(cs_name)
|
||||||
|
elif cat == "passive":
|
||||||
|
passive.append(cs_name)
|
||||||
|
elif cat == "non_living":
|
||||||
|
non_living.append(cs_name)
|
||||||
|
else:
|
||||||
|
non_living.append(cs_name)
|
||||||
|
else:
|
||||||
|
non_living.append(cs_name)
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"version": root.name.replace("-decompiled", "").replace("-client", ""),
|
||||||
|
"hostile": sorted(hostile),
|
||||||
|
"passive": sorted(passive),
|
||||||
|
"neutral": sorted(neutral),
|
||||||
|
"non_living": sorted(non_living),
|
||||||
|
}
|
||||||
|
|
||||||
|
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(OUTPUT_PATH, 'w') as f:
|
||||||
|
json.dump(output, f, indent=2)
|
||||||
|
|
||||||
|
print(f"\nGenerated {OUTPUT_PATH}")
|
||||||
|
print(f" hostile: {len(hostile)}")
|
||||||
|
print(f" passive: {len(passive)}")
|
||||||
|
print(f" neutral: {len(neutral)}")
|
||||||
|
print(f" non_living: {len(non_living)}")
|
||||||
|
print(f" total: {len(hostile) + len(passive) + len(neutral) + len(non_living)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue