mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: load chat translations from server resource packs
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/d7467a13-d1ff-4cd6-a332-1ac389c445da Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
This commit is contained in:
parent
9c47456455
commit
34f45543c3
2 changed files with 193 additions and 9 deletions
|
|
@ -580,7 +580,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
case ConfigurationPacketTypesIn.RemoveResourcePack:
|
||||
if (dataTypes.ReadNextBool(packetData)) // Has UUID
|
||||
dataTypes.ReadNextUUID(packetData); // UUID
|
||||
ChatParser.RemoveResourcePackTranslations(dataTypes.ReadNextUUID(packetData).ToString("D")); // UUID
|
||||
else
|
||||
ChatParser.ClearResourcePackTranslations();
|
||||
break;
|
||||
|
||||
case ConfigurationPacketTypesIn.ResourcePack:
|
||||
|
|
@ -703,6 +705,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
var url = dataTypes.ReadNextString(packetData);
|
||||
var hash = dataTypes.ReadNextString(packetData);
|
||||
string packIdentifier = uuid != Guid.Empty ? uuid.ToString("D") : (hash.Length == 40 ? hash : url);
|
||||
|
||||
if (protocolVersion >= MC_1_17_Version)
|
||||
{
|
||||
|
|
@ -740,6 +743,8 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
SendPacket(PacketTypesOut.ResourcePackStatus, acceptedResourcePackData); // Accepted
|
||||
SendPacket(PacketTypesOut.ResourcePackStatus, loadedResourcePackData); // Successfully loaded
|
||||
}
|
||||
|
||||
ChatParser.LoadResourcePackTranslations(packIdentifier, url, hash);
|
||||
}
|
||||
|
||||
private bool HandlePlayPackets(int packetId, Queue<byte> packetData)
|
||||
|
|
@ -2447,7 +2452,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
break;
|
||||
case PacketTypesIn.RemoveResourcePack:
|
||||
if (dataTypes.ReadNextBool(packetData)) // Has UUID
|
||||
dataTypes.ReadNextUUID(packetData); // UUID
|
||||
ChatParser.RemoveResourcePackTranslations(dataTypes.ReadNextUUID(packetData).ToString("D")); // UUID
|
||||
else
|
||||
ChatParser.ClearResourcePackTranslations();
|
||||
break;
|
||||
case PacketTypesIn.ResourcePackSend:
|
||||
HandleResourcePackPacket(packetData);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
|
@ -253,12 +255,24 @@ namespace MinecraftClient.Protocol.Message
|
|||
/// </summary>
|
||||
private static Dictionary<string, string> TranslationRules = new();
|
||||
|
||||
private sealed class ResourcePackTranslationLayer(string packIdentifier, Dictionary<string, string> translations)
|
||||
{
|
||||
public string PackIdentifier { get; } = packIdentifier;
|
||||
public Dictionary<string, string> Translations { get; } = translations;
|
||||
}
|
||||
|
||||
private const long MaxResourcePackDownloadBytes = 256L * 1024 * 1024;
|
||||
|
||||
private static readonly List<ResourcePackTranslationLayer> ResourcePackTranslationLayers = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initialize translation rules.
|
||||
/// Necessary for properly printing some chat messages.
|
||||
/// </summary>
|
||||
public static void InitTranslations()
|
||||
{
|
||||
ResourcePackTranslationLayers.Clear();
|
||||
|
||||
if (!RulesInitialized)
|
||||
{
|
||||
InitRules();
|
||||
|
|
@ -379,10 +393,60 @@ namespace MinecraftClient.Protocol.Message
|
|||
|
||||
public static string? TranslateString(string rulename)
|
||||
{
|
||||
if (TranslationRules.TryGetValue(rulename, out string? result))
|
||||
return result;
|
||||
else
|
||||
return null;
|
||||
return TryGetTranslationRule(rulename, out string? result) ? result : null;
|
||||
}
|
||||
|
||||
public static void LoadResourcePackTranslations(string packIdentifier, string url, string hash)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(packIdentifier);
|
||||
ArgumentException.ThrowIfNullOrEmpty(url);
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? resourcePackUri)
|
||||
|| resourcePackUri.Scheme is not "http" and not "https")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string temporaryFilePath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
DownloadResourcePack(resourcePackUri, hash, temporaryFilePath);
|
||||
using FileStream resourcePackFile = File.OpenRead(temporaryFilePath);
|
||||
LoadResourcePackTranslations(packIdentifier, resourcePackFile);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(temporaryFilePath);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveResourcePackTranslations(string packIdentifier)
|
||||
{
|
||||
ResourcePackTranslationLayers.RemoveAll(layer =>
|
||||
layer.PackIdentifier.Equals(packIdentifier, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public static void ClearResourcePackTranslations()
|
||||
{
|
||||
ResourcePackTranslationLayers.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -400,10 +464,9 @@ namespace MinecraftClient.Protocol.Message
|
|||
RulesInitialized = true;
|
||||
}
|
||||
|
||||
if (TranslationRules.ContainsKey(rulename))
|
||||
if (TryGetTranslationRule(rulename, out string? rule))
|
||||
{
|
||||
int using_idx = 0;
|
||||
string rule = TranslationRules[rulename];
|
||||
StringBuilder result = new();
|
||||
for (int i = 0; i < rule.Length; i++)
|
||||
{
|
||||
|
|
@ -445,6 +508,120 @@ namespace MinecraftClient.Protocol.Message
|
|||
else return "[" + rulename + "] " + string.Join(" ", using_data);
|
||||
}
|
||||
|
||||
private static bool TryGetTranslationRule(string rulename, out string? result)
|
||||
{
|
||||
for (int i = ResourcePackTranslationLayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (ResourcePackTranslationLayers[i].Translations.TryGetValue(rulename, out result))
|
||||
return true;
|
||||
}
|
||||
|
||||
return TranslationRules.TryGetValue(rulename, out result);
|
||||
}
|
||||
|
||||
private static void DownloadResourcePack(Uri resourcePackUri, string hash, string temporaryFilePath)
|
||||
{
|
||||
using HttpClient httpClient = new();
|
||||
using HttpResponseMessage response =
|
||||
httpClient.GetAsync(resourcePackUri, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult();
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using Stream resourcePackStream = response.Content.ReadAsStream();
|
||||
using FileStream temporaryFile = File.Create(temporaryFilePath);
|
||||
using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA1);
|
||||
|
||||
byte[] buffer = new byte[81920];
|
||||
long totalBytes = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int bytesRead = resourcePackStream.Read(buffer, 0, buffer.Length);
|
||||
if (bytesRead <= 0)
|
||||
break;
|
||||
|
||||
totalBytes += bytesRead;
|
||||
if (totalBytes > MaxResourcePackDownloadBytes)
|
||||
throw new InvalidDataException();
|
||||
|
||||
temporaryFile.Write(buffer, 0, bytesRead);
|
||||
|
||||
if (hash.Length == 40)
|
||||
incrementalHash.AppendData(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
if (hash.Length == 40)
|
||||
{
|
||||
string downloadedHash = Convert.ToHexString(incrementalHash.GetHashAndReset());
|
||||
if (!downloadedHash.Equals(hash, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadResourcePackTranslations(string packIdentifier, Stream resourcePackStream)
|
||||
{
|
||||
var fallbackTranslations = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var selectedLanguageTranslations = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
string selectedLanguage = Config.Main.Advanced.Language;
|
||||
|
||||
using ZipArchive archive = new(resourcePackStream, ZipArchiveMode.Read, leaveOpen: true);
|
||||
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
if (!TryGetResourcePackLanguage(entry.FullName, out string? language))
|
||||
continue;
|
||||
|
||||
if (language.Equals("en_us", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
MergeResourcePackTranslations(entry, fallbackTranslations);
|
||||
}
|
||||
else if (language.Equals(selectedLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
MergeResourcePackTranslations(entry, selectedLanguageTranslations);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in selectedLanguageTranslations)
|
||||
fallbackTranslations[entry.Key] = entry.Value;
|
||||
|
||||
RemoveResourcePackTranslations(packIdentifier);
|
||||
|
||||
if (fallbackTranslations.Count > 0)
|
||||
ResourcePackTranslationLayers.Add(new ResourcePackTranslationLayer(packIdentifier, fallbackTranslations));
|
||||
}
|
||||
|
||||
private static bool TryGetResourcePackLanguage(string entryPath, out string? language)
|
||||
{
|
||||
language = null;
|
||||
|
||||
string[] pathParts = entryPath
|
||||
.Replace('\\', '/')
|
||||
.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (pathParts.Length != 4
|
||||
|| !pathParts[0].Equals("assets", StringComparison.OrdinalIgnoreCase)
|
||||
|| !pathParts[2].Equals("lang", StringComparison.OrdinalIgnoreCase)
|
||||
|| !pathParts[3].EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
language = Path.GetFileNameWithoutExtension(pathParts[3]);
|
||||
return !string.IsNullOrEmpty(language);
|
||||
}
|
||||
|
||||
private static void MergeResourcePackTranslations(ZipArchiveEntry entry, Dictionary<string, string> translations)
|
||||
{
|
||||
using Stream entryStream = entry.Open();
|
||||
Dictionary<string, string>? entryTranslations =
|
||||
JsonSerializer.Deserialize<Dictionary<string, string>>(entryStream);
|
||||
|
||||
if (entryTranslations is null)
|
||||
return;
|
||||
|
||||
foreach (var (key, value) in entryTranslations)
|
||||
translations[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mapping from JSON/NBT property names to Minecraft formatting codes (without §).
|
||||
/// Both "underlined" (canonical Minecraft name) and "underline" (alias) are supported.
|
||||
|
|
@ -633,4 +810,4 @@ namespace MinecraftClient.Protocol.Message
|
|||
return formatting + message + extraBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue