diff --git a/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/docs/Document.java b/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/docs/Document.java index 223fa9cb9..e2c39ad8a 100644 --- a/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/docs/Document.java +++ b/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/docs/Document.java @@ -97,38 +97,46 @@ public record Document(DocumentFileTree directory, String macroName = matcher.group("name"); String endLine = ""; - var properties = new LinkedHashMap>(); var lines = new ArrayList(); + while (true) { + line = reader.readLine(); + + if (line == null) + throw new IOException("Missing end line for macro: " + macroName); + else if (line.startsWith("` and `` lines. +/// +/// For example, if you create a document `FOO.md` and translate it into Simplified Chinese, Traditional Chinese, and Japanese, +/// you can add the following content in these files to create links to other language versions: +/// +/// ```markdown +/// +/// +/// ``` +/// +/// After running `./gradlew updateDocuments`, the macro processor will automatically update the content between these two lines: +/// +/// ``` +/// +/// **English** | 中文 ([简体](FOO_zh.md), [繁體](FOO_zh_Hant.md)) | [日本語](FOO_ja.md) +/// +/// ``` +/// /// @author Glavo public enum MacroProcessor { + /// Does not process the content in any way. + /// + /// Supported properties: + /// + /// - `NAME`: The name of this block (used by other macros). + /// - `PROCESS_LINK`: If set to `FALSE`, document links in the content will not be automatically updated. + BLOCK { + @Override + public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException { + var mutableProperties = new LinkedHashMap<>(macroBlock.properties()); + MacroProcessor.removeSingleProperty(mutableProperties, "NAME"); + boolean processLink = !"FALSE".equalsIgnoreCase(MacroProcessor.removeSingleProperty(mutableProperties, "PROCESS_LINK")); + + if (!mutableProperties.isEmpty()) + throw new IllegalArgumentException("Unsupported properties: " + mutableProperties.keySet()); + + MacroProcessor.writeBegin(outputBuilder, macroBlock); + MacroProcessor.writeProperties(outputBuilder, macroBlock); + for (String line : macroBlock.contentLines()) { + if (processLink) + MacroProcessor.processLine(outputBuilder, line, document); + else + outputBuilder.append(line).append('\n'); + } + MacroProcessor.writeEnd(outputBuilder, macroBlock); + } + }, + + /// Used to automatically generate links to other language versions of the current document. + /// + /// Does not support any properties. LANGUAGE_SWITCHER { private static boolean containsIdentity(List list, T element) { for (T t : list) { @@ -104,17 +154,187 @@ public enum MacroProcessor { MacroProcessor.writeEnd(outputBuilder, macroBlock); } }, - BLOCK { + + /// Copy the block with the specified name from the English version of the current document. + /// + /// Supported properties: + /// + /// - `NAME` (required): Specifies the block to be copied. + /// - `REPLACE` (repeatable): Used to replace specified text. Accepts a list containing two strings. The first string is a regular expression for matching content; the second string is the replacement target. + /// - `PROCESS_LINK`: If set to `FALSE`, document links in the content will not be automatically updated. + COPY { + private record Replace(Pattern pattern, String replacement) { + } + + private static IllegalArgumentException illegalReplace(String value) { + return new IllegalArgumentException("Illegal replacement pattern: " + value); + } + + private static Replace parseReplace(String value) { + List list = MacroProcessor.parseStringList(value); + if (list.size() != 2) + throw illegalReplace(value); + + return new Replace(Pattern.compile(list.get(0)), list.get(1)); + } + @Override public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException { + var mutableProperties = new LinkedHashMap<>(macroBlock.properties()); + String blockName = MacroProcessor.removeSingleProperty(mutableProperties, "NAME"); + if (blockName == null) + throw new IllegalArgumentException("Missing property: NAME"); + + List replaces = Objects.requireNonNullElse(mutableProperties.remove("REPLACE"), List.of()) + .stream() + .map(it -> parseReplace(it)) + .toList(); + + boolean processLink = !"FALSE".equalsIgnoreCase(MacroProcessor.removeSingleProperty(mutableProperties, "PROCESS_LINK")); + + if (!mutableProperties.isEmpty()) + throw new IllegalArgumentException("Unsupported properties: " + mutableProperties.keySet()); + + LocalizedDocument localizedDocument = document.directory().getFiles().get(document.name()); + Document fromDocument; + if (localizedDocument == null || (fromDocument = localizedDocument.getDocuments().get(DocumentLocale.ENGLISH)) == null) + throw new IOException("Document " + document.name() + " for english does not exist"); + + List nameList = List.of(blockName); + + var fromBlock = (Document.MacroBlock) fromDocument.items().stream() + .filter(it -> it instanceof Document.MacroBlock macro + && macro.name().equals(BLOCK.name()) + && nameList.equals(macro.properties().get("NAME")) + ) + .findFirst() + .orElseThrow(() -> new IOException("Cannot find the block \"" + blockName + "\" in " + fromDocument.file())); + MacroProcessor.writeBegin(outputBuilder, macroBlock); MacroProcessor.writeProperties(outputBuilder, macroBlock); - for (String line : macroBlock.contentLines()) { - outputBuilder.append(line).append('\n'); + for (String line : fromBlock.contentLines()) { + for (Replace replace : replaces) { + line = replace.pattern.matcher(line).replaceAll(replace.replacement()); + } + if (processLink) + processLine(outputBuilder, line, document); + else + outputBuilder.append(line).append('\n'); } MacroProcessor.writeEnd(outputBuilder, macroBlock); } - }; + }, + ; + + private static String removeSingleProperty(Map> properties, String name) { + List values = properties.remove(name); + if (values == null || values.isEmpty()) + return null; + + if (values.size() != 1) + throw new IllegalArgumentException("Unexpected number of property " + name + ": " + values.size()); + + return values.get(0); + } + + private static List parseStringList(String str) { + if (str.isBlank()) { + return new ArrayList<>(); + } + + // Split the string with ' and space cleverly. + ArrayList parts = new ArrayList<>(2); + + boolean hasValue = false; + StringBuilder current = new StringBuilder(str.length()); + for (int i = 0; i < str.length(); ) { + char c = str.charAt(i); + if (c == '\'' || c == '"') { + hasValue = true; + int end = str.indexOf(c, i + 1); + if (end < 0) { + end = str.length(); + } + current.append(str, i + 1, end); + i = end + 1; + + } else if (c == ' ' || c == '\t') { + if (hasValue) { + parts.add(current.toString()); + current.setLength(0); + hasValue = false; + } + i++; + } else { + hasValue = true; + current.append(c); + i++; + } + } + if (hasValue) + parts.add(current.toString()); + + return parts; + } + + private static final Pattern LINK_PATTERN = Pattern.compile( + "(?<=]\\()[a-zA-Z0-9_\\-./]+\\.md(?=\\))" + ); + + static void processLine(StringBuilder outputBuilder, String line, Document document) { + outputBuilder.append(LINK_PATTERN.matcher(line).replaceAll(matchResult -> { + String rawLink = matchResult.group(); + String[] splitPath = rawLink.split("/"); + + if (splitPath.length == 0) + return rawLink; + + String fileName = splitPath[splitPath.length - 1]; + if (!fileName.endsWith(".md")) + return rawLink; + + DocumentFileTree current = document.directory(); + for (int i = 0; i < splitPath.length - 1; i++) { + String name = splitPath[i]; + switch (name) { + case "" -> { + return rawLink; + } + case "." -> { + continue; + } + case ".." -> { + current = current.getParent(); + if (current == null) + return rawLink; + } + default -> { + current = current.getChildren().get(name); + if (current == null) + return rawLink; + } + } + } + + DocumentLocale.LocaleAndName currentLocaleAndName = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length())); + LocalizedDocument localizedDocument = current.getFiles().get(currentLocaleAndName.name()); + if (localizedDocument != null) { + List candidateLocales = document.locale().getCandidates(); + for (DocumentLocale candidateLocale : candidateLocales) { + if (candidateLocale == currentLocaleAndName.locale()) + return rawLink; + + Document targetDoc = localizedDocument.getDocuments().get(candidateLocale); + if (targetDoc != null) { + splitPath[splitPath.length - 1] = targetDoc.file().getFileName().toString(); + return String.join("/", splitPath); + } + } + } + + return rawLink; + })).append('\n'); + } private static void writeBegin(StringBuilder builder, Document.MacroBlock macroBlock) throws IOException { builder.append(" + + | | Windows | Linux | macOS | FreeBSD | |----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------| | x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) | @@ -16,6 +18,7 @@ | LoongArch64 (Old World) | / | 👌 (Minecraft 1.6~1.20.1) | / | / | | PowerPC-64 (Little-Endian) | / | ❔ | / | / | | S390x | / | ❔ | / | / | + Legend: diff --git a/docs/PLATFORM_zh.md b/docs/PLATFORM_zh.md index dcdef58ee..2732ccb85 100644 --- a/docs/PLATFORM_zh.md +++ b/docs/PLATFORM_zh.md @@ -4,18 +4,23 @@ [English](PLATFORM.md) | **中文** (**简体**, [繁體](PLATFORM_zh_Hant.md)) -| | Windows | Linux | macOS | FreeBSD | -|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------| -| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) | -| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / | -| ARM64 | 👌 (Minecraft 1.8~1.18.2)
✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)
✅ (Minecraft 1.19+)
✅ (使用 Rosetta 2) | ❔ | -| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / | -| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / | -| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / | -| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / | -| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / | -| PowerPC-64 (Little-Endian) | / | ❔ | / | / | -| S390x | / | ❔ | / | / | + + + + +| | Windows | Linux | macOS | FreeBSD | +|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------| +| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) | +| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / | +| ARM64 | 👌 (Minecraft 1.8~1.18.2)
✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)
✅ (Minecraft 1.19+)
✅ (使用 Rosetta 2) | ❔ | +| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / | +| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / | +| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / | +| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / | +| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / | +| PowerPC-64 (Little-Endian) | / | ❔ | / | / | +| S390x | / | ❔ | / | / | + 图例: diff --git a/docs/PLATFORM_zh_Hant.md b/docs/PLATFORM_zh_Hant.md index 46cc95521..b83d277bc 100644 --- a/docs/PLATFORM_zh_Hant.md +++ b/docs/PLATFORM_zh_Hant.md @@ -4,18 +4,23 @@ [English](PLATFORM.md) | **中文** ([简体](PLATFORM_zh.md), **繁體**) -| | Windows | Linux | macOS | FreeBSD | -|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------| -| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) | -| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / | -| ARM64 | 👌 (Minecraft 1.8~1.18.2)
✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)
✅ (Minecraft 1.19+)
✅ (使用 Rosetta 2) | ❔ | -| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / | -| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / | -| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / | -| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / | -| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / | -| PowerPC-64 (Little-Endian) | / | ❔ | / | / | -| S390x | / | ❔ | / | / | + + + + +| | Windows | Linux | macOS | FreeBSD | +|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------| +| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) | +| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / | +| ARM64 | 👌 (Minecraft 1.8~1.18.2)
✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)
✅ (Minecraft 1.19+)
✅ (使用 Rosetta 2) | ❔ | +| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / | +| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / | +| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / | +| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / | +| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / | +| PowerPC-64 (Little-Endian) | / | ❔ | / | / | +| S390x | / | ❔ | / | / | + 圖例: diff --git a/docs/README.md b/docs/README.md index b75985c21..769d504eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + **English** | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md) diff --git a/docs/README_es.md b/docs/README_es.md index f5bc19ff1..a347544f0 100644 --- a/docs/README_es.md +++ b/docs/README_es.md @@ -1,10 +1,14 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + + [English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md) diff --git a/docs/README_ja.md b/docs/README_ja.md index db812d6d6..92a509d12 100644 --- a/docs/README_ja.md +++ b/docs/README_ja.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + [English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md) diff --git a/docs/README_lzh.md b/docs/README_lzh.md index 6ec3d0238..9173207cb 100644 --- a/docs/README_lzh.md +++ b/docs/README_lzh.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + [English](README.md) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md) diff --git a/docs/README_ru.md b/docs/README_ru.md index d056397d8..e022e20f9 100644 --- a/docs/README_ru.md +++ b/docs/README_ru.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + [English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md) diff --git a/docs/README_uk.md b/docs/README_uk.md index d7fca7896..1c901946f 100644 --- a/docs/README_uk.md +++ b/docs/README_uk.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + [English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська** diff --git a/docs/README_zh.md b/docs/README_zh.md index 8ec858d8d..2cbb2b49a 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + [English](README.md) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md) diff --git a/docs/README_zh_Hant.md b/docs/README_zh_Hant.md index 752d53060..a967e999e 100644 --- a/docs/README_zh_Hant.md +++ b/docs/README_zh_Hant.md @@ -1,10 +1,13 @@ # Hello Minecraft! Launcher + + [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL) ![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat) ![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat) [![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U) [![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html) + [English](README.md) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)