mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Cave mode for minimap
This commit is contained in:
parent
1e35a4c624
commit
eae96a8fbc
8 changed files with 379 additions and 21 deletions
|
|
@ -11,7 +11,7 @@ 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 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] | minimap cave [auto|on|off]";
|
||||
public override string CmdDesc => Translations.cmd_minimap_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
|
|
@ -78,6 +78,14 @@ namespace MinecraftClient.Commands
|
|||
.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("cave")
|
||||
.Executes(r => DoCaveInfo(r.Source))
|
||||
.Then(l => l.Literal("auto")
|
||||
.Executes(r => DoCaveSet(r.Source, CaveModeOption.auto)))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoCaveSet(r.Source, CaveModeOption.on)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoCaveSet(r.Source, CaveModeOption.off))))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help")?.GetChild(CmdName)))
|
||||
|
|
@ -251,6 +259,26 @@ namespace MinecraftClient.Commands
|
|||
string.Format(Translations.cmd_minimap_position_set, pos));
|
||||
}
|
||||
|
||||
private static int DoCaveInfo(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
var mode = view.GetMinimapCaveMode();
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_cave_current, mode));
|
||||
}
|
||||
|
||||
private static int DoCaveSet(CmdResult r, CaveModeOption mode)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() => view.SetMinimapCaveMode(mode));
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_cave_set, mode));
|
||||
}
|
||||
|
||||
private static string BoolStr(bool v) => v ? "ON" : "OFF";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -975,6 +975,9 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
|
|||
<data name="Console.Minimap.RefreshInterval" xml:space="preserve">
|
||||
<value>Minimap refresh interval in milliseconds (100-5000).</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.CaveMode" xml:space="preserve">
|
||||
<value>Cave rendering mode: "auto" (detect ceiling), "on" (always cave view), "off" (always surface view).</value>
|
||||
</data>
|
||||
<data name="Main.General.AuthlibUser" xml:space="preserve">
|
||||
<value>Yggdrasil authlib multi-user selection.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -7187,6 +7187,24 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current cave mode: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_cave_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.cave_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cave mode set to: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_cave_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.cave_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to list achievements/advancements from the server..
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -2533,6 +2533,12 @@ see item details.</value>
|
|||
<data name="cmd.minimap.position_set" xml:space="preserve">
|
||||
<value>Minimap position set to: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.cave_current" xml:space="preserve">
|
||||
<value>Current cave mode: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.cave_set" xml:space="preserve">
|
||||
<value>Cave mode set to: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.desc" xml:space="preserve">
|
||||
<value>list achievements/advancements from the server.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -1286,6 +1286,9 @@ namespace MinecraftClient
|
|||
[TomlInlineComment("$Console.Minimap.RefreshInterval$")]
|
||||
public int RefreshInterval = Tui.MinimapControl.DefaultRefreshMs;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.CaveMode$")]
|
||||
public Tui.CaveModeOption CaveMode = Tui.CaveModeOption.auto;
|
||||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
Zoom = Math.Clamp(Zoom, Tui.MinimapControl.MinZoom, Tui.MinimapControl.MaxZoom);
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ namespace MinecraftClient.Tui
|
|||
_minimapControl.NameConfig.Hostile = mmCfg.ShowHostileNames;
|
||||
_minimapControl.NameConfig.Neutral = mmCfg.ShowNeutralNames;
|
||||
_minimapControl.NameConfig.Passive = mmCfg.ShowPassiveNames;
|
||||
_minimapControl.CaveMode = mmCfg.CaveMode;
|
||||
|
||||
var (hAlign, vAlign, margin) = GetMinimapAlignment(mmCfg.Position);
|
||||
_minimapBorder = new Border
|
||||
|
|
@ -1097,6 +1098,14 @@ namespace MinecraftClient.Tui
|
|||
|
||||
public MinimapPosition GetMinimapPosition() => Settings.Config.Console.Minimap.Position;
|
||||
|
||||
public void SetMinimapCaveMode(CaveModeOption mode)
|
||||
{
|
||||
_minimapControl.CaveMode = mode;
|
||||
Settings.Config.Console.Minimap.CaveMode = mode;
|
||||
}
|
||||
|
||||
public CaveModeOption GetMinimapCaveMode() => _minimapControl.CaveMode;
|
||||
|
||||
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)),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ namespace MinecraftClient.Tui
|
|||
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);
|
||||
public static readonly Color CaveBorderColor = Color.FromRgb(16, 16, 16);
|
||||
public static readonly Color CaveSolidColor = Color.FromRgb(24, 20, 18);
|
||||
|
||||
private static readonly FrozenDictionary<Material, Color> ColorTable;
|
||||
private static readonly FrozenSet<Material> FullyTransparentMats;
|
||||
|
|
@ -114,6 +116,14 @@ namespace MinecraftClient.Tui
|
|||
|
||||
public static bool IsFullyTransparent(Material m) => FullyTransparentMats.Contains(m);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true for materials that block light propagation (solid, liquids),
|
||||
/// used by cave mode to find the surface from the player's Y level.
|
||||
/// Mirrors VoxelMap's lightDampening > 0 check.
|
||||
/// </summary>
|
||||
public static bool IsLightBlocking(Material m)
|
||||
=> (m == Material.Lava) || (!FullyTransparentMats.Contains(m) && m.IsSolid());
|
||||
|
||||
public static bool IsWater(Material m) => WaterMats.Contains(m);
|
||||
|
||||
public static bool IsIce(Material m) => IceMats.Contains(m);
|
||||
|
|
@ -137,8 +147,8 @@ namespace MinecraftClient.Tui
|
|||
int multiplier = heightDelta switch
|
||||
{
|
||||
> 0 => 255, // higher than neighbor: brightest
|
||||
0 => 220, // same height: normal
|
||||
_ => 180, // lower than neighbor: darker
|
||||
0 => 220, // same height: normal
|
||||
_ => 180, // lower than neighbor: darker
|
||||
};
|
||||
byte r = (byte)(baseColor.R * multiplier / 255);
|
||||
byte g = (byte)(baseColor.G * multiplier / 255);
|
||||
|
|
@ -157,6 +167,19 @@ namespace MinecraftClient.Tui
|
|||
return Blend(IceColor, bottomColor, 0.35);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Darken a color to simulate underground lighting. Cave floors receive
|
||||
/// a minimum brightness of ~32/255 for non-solid blocks (matching VoxelMap),
|
||||
/// while solid/unreachable columns render as near-black.
|
||||
/// </summary>
|
||||
public static Color ApplyCaveDarkening(Color baseColor, double factor = 0.55)
|
||||
{
|
||||
byte r = (byte)(baseColor.R * factor);
|
||||
byte g = (byte)(baseColor.G * factor);
|
||||
byte b = (byte)(baseColor.B * factor);
|
||||
return Color.FromRgb(r, g, b);
|
||||
}
|
||||
|
||||
private static Color Blend(Color top, Color bottom, double topAlpha)
|
||||
{
|
||||
byte r = (byte)(top.R * topAlpha + bottom.R * (1.0 - topAlpha));
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ using MinecraftClient.Mapping;
|
|||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public enum CaveModeOption { auto, on, off }
|
||||
|
||||
/// <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).
|
||||
|
|
@ -63,6 +65,8 @@ namespace MinecraftClient.Tui
|
|||
|
||||
public MinimapPosition Position { get; set; } = MinimapPosition.top_right;
|
||||
|
||||
public CaveModeOption CaveMode { get; set; } = CaveModeOption.auto;
|
||||
|
||||
public int MapPixelWidth => _mapWidth;
|
||||
public int MapPixelHeight => _mapHeight;
|
||||
|
||||
|
|
@ -177,19 +181,21 @@ namespace MinecraftClient.Tui
|
|||
bool showHostile = _nameConfig.Hostile;
|
||||
bool showNeutral = _nameConfig.Neutral;
|
||||
bool showPassive = _nameConfig.Passive;
|
||||
var caveOpt = CaveMode;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = SampleTerrain(client, bpp, w, h,
|
||||
showPlayers, showHostile, showNeutral, showPassive, ct);
|
||||
showPlayers, showHostile, showNeutral, showPassive, caveOpt, ct);
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ApplyPixelBuffer(result, w, h);
|
||||
UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w);
|
||||
UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w,
|
||||
result.CaveModeActive);
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
|
|
@ -236,6 +242,7 @@ namespace MinecraftClient.Tui
|
|||
public int CenterX;
|
||||
public int CenterY;
|
||||
public int Bpp;
|
||||
public bool CaveModeActive;
|
||||
}
|
||||
|
||||
private static bool ShouldShowNameLocal(MobCategory cat,
|
||||
|
|
@ -253,7 +260,7 @@ namespace MinecraftClient.Tui
|
|||
|
||||
private static SampleResult SampleTerrain(McClient client, int bpp, int mapW, int mapH,
|
||||
bool showPlayers, bool showHostile, bool showNeutral, bool showPassive,
|
||||
CancellationToken ct)
|
||||
CaveModeOption caveOpt, CancellationToken ct)
|
||||
{
|
||||
var result = new SampleResult
|
||||
{
|
||||
|
|
@ -281,6 +288,9 @@ namespace MinecraftClient.Tui
|
|||
int minY = dim.minY;
|
||||
int scanTop = Math.Min(playerBlockY + 32, dim.maxY - 1);
|
||||
|
||||
bool caveMode = ResolveCaveMode(caveOpt, world, dim, playerBlockX, playerBlockY, playerBlockZ, scanTop);
|
||||
result.CaveModeActive = caveMode;
|
||||
|
||||
var entities = client.GetEntityHandlingEnabled()
|
||||
? client.GetEntities()
|
||||
: null;
|
||||
|
|
@ -374,6 +384,8 @@ namespace MinecraftClient.Tui
|
|||
ChunkColumn? cachedColumn = null;
|
||||
int cachedChunkX = int.MinValue, cachedChunkZ = int.MinValue;
|
||||
|
||||
bool[,]? caveMask = caveMode ? new bool[mapW, mapH] : null;
|
||||
|
||||
for (int px = 0; px < mapW; px++)
|
||||
{
|
||||
for (int py = 0; py < mapH; py++)
|
||||
|
|
@ -383,23 +395,51 @@ namespace MinecraftClient.Tui
|
|||
int baseX = playerBlockX + (px - centerX) * bpp;
|
||||
int baseZ = playerBlockZ + (py - centerY) * bpp;
|
||||
|
||||
if (bpp == 1)
|
||||
if (caveMode)
|
||||
{
|
||||
var (color, surfY, surfMat) = SampleColumn(world, baseX, baseZ, scanTop, minY,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
result.Pixels[px, py] = color;
|
||||
result.Heights[px, py] = surfY;
|
||||
result.BlockTypes![px, py] = surfMat;
|
||||
if (bpp == 1)
|
||||
{
|
||||
var (color, surfY, surfMat, inCave) = SampleColumnCave(
|
||||
world, baseX, baseZ, playerBlockY, minY, dim.maxY - 1,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
result.Pixels[px, py] = color;
|
||||
result.Heights[px, py] = surfY;
|
||||
result.BlockTypes![px, py] = surfMat;
|
||||
caveMask![px, py] = inCave;
|
||||
}
|
||||
else
|
||||
{
|
||||
var (color, surfY, matSum, inCave) = SampleAreaDominantCave(
|
||||
world, baseX, baseZ, bpp, playerBlockY, minY, dim.maxY - 1,
|
||||
result.BlockSummary is not null,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
result.Pixels[px, py] = color;
|
||||
result.Heights[px, py] = surfY;
|
||||
if (result.BlockSummary is not null)
|
||||
result.BlockSummary[px, py] = matSum;
|
||||
caveMask![px, py] = inCave;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (color, surfY, matSum) = SampleAreaDominant(world, baseX, baseZ, bpp,
|
||||
scanTop, minY, result.BlockSummary is not null,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
result.Pixels[px, py] = color;
|
||||
result.Heights[px, py] = surfY;
|
||||
if (result.BlockSummary is not null)
|
||||
result.BlockSummary[px, py] = matSum;
|
||||
if (bpp == 1)
|
||||
{
|
||||
var (color, surfY, surfMat) = SampleColumn(world, baseX, baseZ, scanTop, minY,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
result.Pixels[px, py] = color;
|
||||
result.Heights[px, py] = surfY;
|
||||
result.BlockTypes![px, py] = surfMat;
|
||||
}
|
||||
else
|
||||
{
|
||||
var (color, surfY, matSum) = SampleAreaDominant(world, baseX, baseZ, bpp,
|
||||
scanTop, minY, result.BlockSummary is not null,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
result.Pixels[px, py] = color;
|
||||
result.Heights[px, py] = surfY;
|
||||
if (result.BlockSummary is not null)
|
||||
result.BlockSummary[px, py] = matSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -416,6 +456,9 @@ namespace MinecraftClient.Tui
|
|||
}
|
||||
}
|
||||
|
||||
if (caveMask is not null)
|
||||
ApplyCaveBorder(result, caveMask, mapW, mapH, entityPixels);
|
||||
|
||||
foreach (var (key, info) in entityPixels)
|
||||
{
|
||||
var (px, py) = key;
|
||||
|
|
@ -640,6 +683,230 @@ namespace MinecraftClient.Tui
|
|||
return (best, avgY, summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether cave mode should be active for this frame.
|
||||
/// Mirrors VoxelMap's detection: hasCeiling dimensions always use cave mode,
|
||||
/// otherwise check whether the player's column has a solid block above.
|
||||
/// </summary>
|
||||
private static bool ResolveCaveMode(CaveModeOption opt, World world, Dimension dim,
|
||||
int playerX, int playerY, int playerZ, int scanTop)
|
||||
{
|
||||
if (opt == CaveModeOption.off) return false;
|
||||
if (opt == CaveModeOption.on) return true;
|
||||
|
||||
if (dim.hasCeiling) return true;
|
||||
|
||||
for (int y = playerY + 2; y <= scanTop; y++)
|
||||
{
|
||||
var mat = world.GetBlock(new Mapping.Location(playerX, y, playerZ)).Type;
|
||||
if (MinimapColorMap.IsLightBlocking(mat))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cave-mode column sampler. Starting from playerY, scans down through air
|
||||
/// to find the first light-blocking block (the cave floor), or scans up if
|
||||
/// the player is embedded in solid. Returns the floor block color with cave
|
||||
/// darkening applied, plus an inCave flag indicating the column has a reachable
|
||||
/// air pocket at the player's Y level.
|
||||
/// </summary>
|
||||
private static (Color color, int surfaceY, Material surfaceMat, bool inCave) SampleColumnCave(
|
||||
World world, int x, int z, int playerY, int minY, int maxY,
|
||||
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, Material.Air, false);
|
||||
|
||||
int caveFloorY = FindCaveFloorY(cachedColumn, x, z, playerY, minY, maxY);
|
||||
|
||||
if (caveFloorY == int.MinValue)
|
||||
{
|
||||
var fallback = SampleColumn(world, x, z, maxY, minY,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
return (MinimapColorMap.CaveSolidColor, fallback.surfaceY, fallback.surfaceMat, false);
|
||||
}
|
||||
|
||||
var loc = new Mapping.Location(x, caveFloorY, z);
|
||||
var chunk = cachedColumn.GetChunk(loc);
|
||||
if (chunk is null)
|
||||
return (MinimapColorMap.CaveSolidColor, caveFloorY, Material.Air, false);
|
||||
|
||||
var block = chunk.GetBlock(loc);
|
||||
var mat = block.Type;
|
||||
var color = MinimapColorMap.GetBaseColor(mat);
|
||||
color = MinimapColorMap.ApplyCaveDarkening(color);
|
||||
|
||||
return (color, caveFloorY, mat, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the cave floor Y at (x, z) by scanning from playerY.
|
||||
/// If the block at playerY is air-like, scan down for the first solid block.
|
||||
/// If the block at playerY is solid, scan up (up to playerY + 10) for the
|
||||
/// first air block, then return that Y (the cave ceiling opening).
|
||||
/// Returns int.MinValue if no cave floor is found.
|
||||
/// </summary>
|
||||
private static int FindCaveFloorY(ChunkColumn column, int x, int z, int playerY, int minY, int maxY)
|
||||
{
|
||||
var startLoc = new Mapping.Location(x, playerY, z);
|
||||
var startChunk = column.GetChunk(startLoc);
|
||||
|
||||
bool startIsAir;
|
||||
if (startChunk is null)
|
||||
{
|
||||
startIsAir = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var startMat = startChunk.GetBlock(startLoc).Type;
|
||||
startIsAir = !MinimapColorMap.IsLightBlocking(startMat);
|
||||
}
|
||||
|
||||
if (startIsAir)
|
||||
{
|
||||
for (int y = playerY - 1; y >= minY; y--)
|
||||
{
|
||||
var loc = new Mapping.Location(x, y, z);
|
||||
var chunk = column.GetChunk(loc);
|
||||
if (chunk is null) continue;
|
||||
|
||||
var mat = chunk.GetBlock(loc).Type;
|
||||
if (MinimapColorMap.IsLightBlocking(mat))
|
||||
return y;
|
||||
}
|
||||
return minY;
|
||||
}
|
||||
else
|
||||
{
|
||||
int upLimit = Math.Min(playerY + 10, maxY);
|
||||
for (int y = playerY + 1; y <= upLimit; y++)
|
||||
{
|
||||
var loc = new Mapping.Location(x, y, z);
|
||||
var chunk = column.GetChunk(loc);
|
||||
if (chunk is null) continue;
|
||||
|
||||
var mat = chunk.GetBlock(loc).Type;
|
||||
if (!MinimapColorMap.IsLightBlocking(mat))
|
||||
{
|
||||
for (int y2 = y - 1; y2 >= minY; y2--)
|
||||
{
|
||||
var loc2 = new Mapping.Location(x, y2, z);
|
||||
var chunk2 = column.GetChunk(loc2);
|
||||
if (chunk2 is null) continue;
|
||||
|
||||
var mat2 = chunk2.GetBlock(loc2).Type;
|
||||
if (MinimapColorMap.IsLightBlocking(mat2))
|
||||
return y2;
|
||||
}
|
||||
return minY;
|
||||
}
|
||||
}
|
||||
return int.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static (Color color, int surfaceY, List<(Material Mat, int Count)>? matSummary, bool inCave)
|
||||
SampleAreaDominantCave(World world, int baseX, int baseZ,
|
||||
int size, int playerY, int minY, int maxY, bool collectMats,
|
||||
ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ)
|
||||
{
|
||||
var colorCounts = new Dictionary<Color, (int Count, int SumY)>();
|
||||
Dictionary<Material, int>? matCounts = collectMats ? [] : null;
|
||||
int caveCount = 0;
|
||||
|
||||
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, surfMat, inCave) = SampleColumnCave(
|
||||
world, baseX + dx, baseZ + dz, playerY, minY, maxY,
|
||||
ref cachedColumn, ref cachedChunkX, ref cachedChunkZ);
|
||||
|
||||
if (inCave) caveCount++;
|
||||
|
||||
if (colorCounts.TryGetValue(c, out var existing))
|
||||
colorCounts[c] = (existing.Count + 1, existing.SumY + surfY);
|
||||
else
|
||||
colorCounts[c] = (1, surfY);
|
||||
|
||||
if (matCounts is not null)
|
||||
{
|
||||
if (matCounts.TryGetValue(surfMat, out int mc))
|
||||
matCounts[surfMat] = mc + 1;
|
||||
else
|
||||
matCounts[surfMat] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
List<(Material, int)>? summary = null;
|
||||
if (matCounts is not null && matCounts.Count > 0)
|
||||
{
|
||||
summary = matCounts
|
||||
.OrderByDescending(kv => kv.Value)
|
||||
.Select(kv => (kv.Key, kv.Value))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
int totalSamples = 0;
|
||||
foreach (var kvp in colorCounts)
|
||||
totalSamples += kvp.Value.Count;
|
||||
|
||||
bool majorityInCave = caveCount * 2 >= totalSamples;
|
||||
return (best, avgY, summary, majorityInCave);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw a 1-pixel dark border around the boundary between cave-reachable pixels
|
||||
/// and non-cave (solid/surface) pixels, giving the cave region a visible edge.
|
||||
/// </summary>
|
||||
private static void ApplyCaveBorder(SampleResult result, bool[,] caveMask,
|
||||
int mapW, int mapH, Dictionary<(int, int), (Color, int)> entityPixels)
|
||||
{
|
||||
for (int px = 0; px < mapW; px++)
|
||||
{
|
||||
for (int py = 0; py < mapH; py++)
|
||||
{
|
||||
if (entityPixels.ContainsKey((px, py))) continue;
|
||||
if (caveMask[px, py]) continue;
|
||||
|
||||
bool neighborInCave = false;
|
||||
if (px > 0 && caveMask[px - 1, py]) neighborInCave = true;
|
||||
if (!neighborInCave && px < mapW - 1 && caveMask[px + 1, py]) neighborInCave = true;
|
||||
if (!neighborInCave && py > 0 && caveMask[px, py - 1]) neighborInCave = true;
|
||||
if (!neighborInCave && py < mapH - 1 && caveMask[px, py + 1]) neighborInCave = true;
|
||||
|
||||
if (neighborInCave)
|
||||
result.Pixels[px, py] = MinimapColorMap.CaveBorderColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPixelBuffer(SampleResult result, int w, int h)
|
||||
{
|
||||
int rows = h / 2;
|
||||
|
|
@ -898,7 +1165,7 @@ namespace MinecraftClient.Tui
|
|||
}
|
||||
|
||||
private void UpdateInfoBarAndLegend(McClient client, int bpp,
|
||||
HashSet<MobCategory> categories, int mapW)
|
||||
HashSet<MobCategory> categories, int mapW, bool caveModeActive)
|
||||
{
|
||||
var loc = client.GetCurrentLocation();
|
||||
float yaw = client.GetYaw();
|
||||
|
|
@ -908,7 +1175,8 @@ namespace MinecraftClient.Tui
|
|||
int y = (int)Math.Floor(loc.Y);
|
||||
int z = (int)Math.Floor(loc.Z);
|
||||
|
||||
string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1";
|
||||
string caveSuffix = caveModeActive ? " \u25bc" : "";
|
||||
string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1{caveSuffix}";
|
||||
|
||||
var legendParts = new List<string>();
|
||||
var legendColors = new List<Color>();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue