feat: add cached resource pack translation setting

Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/70cfa855-cff8-495e-ac06-9a65bb5cc913

Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-04 12:32:13 +00:00 committed by GitHub
parent 9f974da00d
commit b87b5abbe2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 133 additions and 7 deletions

View file

@ -261,8 +261,18 @@ namespace MinecraftClient.Protocol.Message
public Dictionary<string, string> Translations { get; } = translations;
}
private sealed class ResourcePackTranslationCacheEntry
{
public string CacheVersion { get; init; } = string.Empty;
public string Language { get; init; } = string.Empty;
public string SourceUrl { get; init; } = string.Empty;
public string SourceHash { get; init; } = string.Empty;
public Dictionary<string, string> Translations { get; init; } = [];
}
private const long MaxResourcePackDownloadBytes = 256L * 1024 * 1024;
private const int ResourcePackDownloadBufferSize = 81920;
private const string ResourcePackTranslationCacheVersion = "1";
private static readonly List<ResourcePackTranslationLayer> ResourcePackTranslationLayers = [];
private static readonly HttpClient ResourcePackHttpClient = new();
@ -403,18 +413,30 @@ namespace MinecraftClient.Protocol.Message
ArgumentException.ThrowIfNullOrEmpty(packIdentifier);
ArgumentException.ThrowIfNullOrEmpty(url);
if (!Config.Main.Advanced.LoadResourcePackTranslations)
return;
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? resourcePackUri)
|| resourcePackUri.Scheme is not "http" and not "https")
{
return;
}
string cacheFilePath = GetResourcePackTranslationCacheFilePath(resourcePackUri, hash);
if (TryLoadCachedResourcePackTranslations(cacheFilePath, resourcePackUri, hash, out Dictionary<string, string>? cachedTranslations))
{
ReplaceResourcePackTranslations(packIdentifier, cachedTranslations);
return;
}
string temporaryFilePath = Path.GetTempFileName();
try
{
DownloadResourcePack(resourcePackUri, hash, temporaryFilePath);
using FileStream resourcePackFile = File.OpenRead(temporaryFilePath);
LoadResourcePackTranslations(packIdentifier, resourcePackFile);
Dictionary<string, string> resourcePackTranslations = ExtractResourcePackTranslations(resourcePackFile);
ReplaceResourcePackTranslations(packIdentifier, resourcePackTranslations);
SaveCachedResourcePackTranslations(cacheFilePath, resourcePackUri, hash, resourcePackTranslations);
}
catch (HttpRequestException)
{
@ -442,7 +464,7 @@ namespace MinecraftClient.Protocol.Message
public static void RemoveResourcePackTranslations(string packIdentifier)
{
ResourcePackTranslationLayers.RemoveAll(layer =>
ResourcePackTranslationLayers.RemoveAll(layer =>
layer.PackIdentifier.Equals(packIdentifier, StringComparison.Ordinal));
}
@ -558,7 +580,7 @@ namespace MinecraftClient.Protocol.Message
}
}
private static void LoadResourcePackTranslations(string packIdentifier, Stream resourcePackStream)
private static Dictionary<string, string> ExtractResourcePackTranslations(Stream resourcePackStream)
{
var mergedTranslations = new Dictionary<string, string>(StringComparer.Ordinal);
var selectedLanguageTranslations = new Dictionary<string, string>(StringComparer.Ordinal);
@ -584,10 +606,7 @@ namespace MinecraftClient.Protocol.Message
foreach (var entry in selectedLanguageTranslations)
mergedTranslations[entry.Key] = entry.Value;
RemoveResourcePackTranslations(packIdentifier);
if (mergedTranslations.Count > 0)
ResourcePackTranslationLayers.Add(new ResourcePackTranslationLayer(packIdentifier, mergedTranslations));
return mergedTranslations;
}
private static bool TryGetResourcePackLanguage(string entryPath, out string? language)
@ -623,6 +642,97 @@ namespace MinecraftClient.Protocol.Message
translations[key] = value;
}
private static void ReplaceResourcePackTranslations(string packIdentifier, Dictionary<string, string> translations)
{
RemoveResourcePackTranslations(packIdentifier);
if (translations.Count > 0)
ResourcePackTranslationLayers.Add(new ResourcePackTranslationLayer(packIdentifier, translations));
}
private static bool TryLoadCachedResourcePackTranslations(string cacheFilePath, Uri resourcePackUri, string hash,
out Dictionary<string, string>? translations)
{
translations = null;
if (!File.Exists(cacheFilePath))
return false;
try
{
using FileStream cacheFile = File.OpenRead(cacheFilePath);
ResourcePackTranslationCacheEntry? cacheEntry =
JsonSerializer.Deserialize<ResourcePackTranslationCacheEntry>(cacheFile);
if (cacheEntry is not null
&& cacheEntry.CacheVersion == ResourcePackTranslationCacheVersion
&& cacheEntry.Language.Equals(Config.Main.Advanced.Language, StringComparison.OrdinalIgnoreCase)
&& cacheEntry.SourceUrl.Equals(resourcePackUri.AbsoluteUri, StringComparison.Ordinal)
&& cacheEntry.SourceHash.Equals(hash, StringComparison.OrdinalIgnoreCase)
&& cacheEntry.Translations.Count > 0)
{
translations = new Dictionary<string, string>(cacheEntry.Translations, StringComparer.Ordinal);
return true;
}
}
catch (IOException)
{
}
catch (JsonException)
{
}
try
{
File.Delete(cacheFilePath);
}
catch (IOException)
{
}
return false;
}
private static void SaveCachedResourcePackTranslations(string cacheFilePath, Uri resourcePackUri, string hash,
Dictionary<string, string> translations)
{
if (translations.Count == 0)
return;
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
ResourcePackTranslationCacheEntry cacheEntry = new()
{
CacheVersion = ResourcePackTranslationCacheVersion,
Language = Config.Main.Advanced.Language,
SourceUrl = resourcePackUri.AbsoluteUri,
SourceHash = hash,
Translations = new Dictionary<string, string>(translations, StringComparer.Ordinal)
};
File.WriteAllText(cacheFilePath, JsonSerializer.Serialize(cacheEntry), Encoding.UTF8);
}
private static string GetResourcePackTranslationCacheFilePath(Uri resourcePackUri, string hash)
{
string cacheKey = GetResourcePackTranslationCacheKey(resourcePackUri, hash);
return Path.Combine("lang", "resourcepacks", $"{cacheKey}.{Config.Main.Advanced.Language}.json");
}
private static string GetResourcePackTranslationCacheKey(Uri resourcePackUri, string hash)
{
if (IsValidSha1(hash))
return hash.ToLowerInvariant();
byte[] urlHash = SHA256.HashData(Encoding.UTF8.GetBytes(resourcePackUri.AbsoluteUri));
return "url-" + Convert.ToHexString(urlHash).ToLowerInvariant();
}
private static bool IsValidSha1(string hash)
{
return hash.Length == 40 && hash.All(Uri.IsHexDigit);
}
/// <summary>
/// Mapping from JSON/NBT property names to Minecraft formatting codes (without §).
/// Both "underlined" (canonical Minecraft name) and "underline" (alias) are supported.

View file

@ -684,6 +684,9 @@ Usage examples: "/tell &lt;mybot&gt; reco Player2", "/connect &lt;serverip&gt; P
<data name="Main.Advanced.LoadMccTrans" xml:space="preserve">
<value>Load translations applied to MCC when available, turn it off to use English only.</value>
</data>
<data name="Main.Advanced.load_resourcepack_translations" xml:space="preserve">
<value>Load translations from server resource packs and cache extracted language entries locally for faster reuse.</value>
</data>
<data name="Main.Advanced.mc_forge" xml:space="preserve">
<value>Use "auto", "no" or "force". Force-enabling only works for MC 1.13+.</value>
</data>

View file

@ -775,6 +775,9 @@ namespace MinecraftClient
[TomlInlineComment("$Main.Advanced.LoadMccTrans$")]
public bool LoadMccTranslation = true;
[TomlInlineComment("$Main.Advanced.load_resourcepack_translations$")]
public bool LoadResourcePackTranslations = true;
// [TomlInlineComment("$Main.Advanced.console_title$")]
public string ConsoleTitle = "%username%@%serverip% - Minecraft Console Client";

View file

@ -269,6 +269,16 @@ Coordinate = { x = 145, y = 64, z = 2045 }
- **Default:** `true`
#### `LoadResourcePackTranslations`
- **Description:**
Set this to `false` to ignore translations provided by server resource packs. When enabled, MCC caches extracted resource-pack translation data locally so future joins can reuse it without downloading the pack again.
- **Type:** `boolean`
- **Default:** `true`
#### `ConsoleTitle`
- **Description:**