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 { /// /// 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. /// 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); public static readonly Color CaveBorderColor = Color.FromRgb(16, 16, 16); public static readonly Color CaveSolidColor = Color.FromRgb(24, 20, 18); private static readonly FrozenDictionary ColorTable; private static readonly FrozenSet FullyTransparentMats; private static readonly FrozenSet WaterMats; private static readonly FrozenSet IceMats; static MinimapColorMap() { var colors = new Dictionary(); var transparent = new HashSet(); var water = new HashSet(); var ice = new HashSet(); 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(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(item.GetString(), out var mat)) transparent.Add(mat); } } if (root.TryGetProperty("water", out var waterEl)) { foreach (var item in waterEl.EnumerateArray()) { if (Enum.TryParse(item.GetString(), out var mat)) water.Add(mat); } } if (root.TryGetProperty("ice", out var iceEl)) { foreach (var item in iceEl.EnumerateArray()) { if (Enum.TryParse(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); /// /// 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. /// 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); public static Color GetBaseColor(Material m) { if (m == Material.Lava) return LavaColor; return ColorTable.GetValueOrDefault(m, DefaultColor); } /// /// 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. /// 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); } /// /// 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. /// 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)); 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); } } }