Compare commits

...

3 Commits

Author SHA1 Message Date
Helvetica Volubi 24d4ebdc7c Update Upstream (Luminol) 2025-06-15 20:48:25 +08:00
Helvetica Volubi 4ca6d9fe09 refactor: I18n update 2025-06-14 16:08:19 +08:00
Helvetica Volubi c35817411a feat: add better shulker-box support (#3) 2025-06-14 05:38:58 +08:00
12 changed files with 537 additions and 320 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ group=me.earthme.lophine
version=1.21.5-R0.1-SNAPSHOT
mcVersion=1.21.5
luminolRef=818e1132dfb8a9849b082f6adf767b66c7da8b31
luminolRef=784ce10986c214df322a9289e8ddb0b700759596
org.gradle.configuration-cache=true
org.gradle.caching=true
@@ -0,0 +1,174 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sat, 14 Jun 2025 05:32:51 +0800
Subject: [PATCH] Better ShulkerBox
You can open shulker box with shift & right click
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
index 1764875b9024569671bf2be537afb8b492f79102..0ae350d7e4b63abad1666c84df6925c71b73a013 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -2351,6 +2351,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
this.containerMenu.removed(this);
this.inventoryMenu.transferState(this.containerMenu);
this.containerMenu = this.inventoryMenu;
+ this.shulkerOpen = false;
}
@Override
diff --git a/net/minecraft/world/entity/player/Inventory.java b/net/minecraft/world/entity/player/Inventory.java
index 9fbd605df26d388c01d2ed9ca6a6138651e7f9a7..e8141cb25d6b9a433890e2cded188c2eb5bcf755 100644
--- a/net/minecraft/world/entity/player/Inventory.java
+++ b/net/minecraft/world/entity/player/Inventory.java
@@ -470,6 +470,16 @@ public class Inventory implements Container, Nameable {
if (equipmentSlot != null) {
this.equipment.set(equipmentSlot, stack);
}
+
+ // Lophine start - better shulker box
+ if (me.earthme.lophine.config.modules.misc.ContainerExpansionConfig.betterShulker && !this.player.pendingClosingShulker) {
+ boolean isMainHand = index == selected;
+ boolean isOffHand = index == 40;
+ if (isMainHand || isOffHand) {
+ me.earthme.lophine.utils.ShulkerBoxesUtil.inventoryCallBack(isMainHand, this.player);
+ }
+ }
+ // Lophine end - better shulker box
}
public ListTag save(ListTag listTag) {
diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java
index 27e335e01d8f09f05a4fea5f15407dbe3a4555cb..fbb9fee8223cc27125e4c0c94a7eb8acab5e9a13 100644
--- a/net/minecraft/world/entity/player/Player.java
+++ b/net/minecraft/world/entity/player/Player.java
@@ -211,6 +211,11 @@ public abstract class Player extends LivingEntity {
public boolean affectsSpawning = true; // Paper - Affects Spawning API
public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; // Paper - flying fall damage
public int enderChestSlotCount = -1; // Purpur - Barrels and enderchests 6 rows
+ // Lophine start - better shulker box
+ public net.minecraft.world.InteractionHand shulkerHand;
+ public boolean pendingClosingShulker = false;
+ public boolean shulkerOpen = false;
+ // Lophine end - better shulker box
// CraftBukkit start
public boolean fauxSleeping;
@@ -595,11 +600,13 @@ public abstract class Player extends LivingEntity {
// Paper start - special close for unloaded inventory
public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
this.containerMenu = this.inventoryMenu;
+ this.shulkerOpen = false;
}
// Paper end - special close for unloaded inventory
public void closeContainer() {
this.containerMenu = this.inventoryMenu;
+ this.shulkerOpen = false;
}
protected void doCloseContainer() {
diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java
index 6c5a01b065c7daa37219a8ab2a471d93e882e756..7b49b35e7a0b9872daceb620cf83b06ae61c3df9 100644
--- a/net/minecraft/world/inventory/AbstractContainerMenu.java
+++ b/net/minecraft/world/inventory/AbstractContainerMenu.java
@@ -697,7 +697,7 @@ public abstract class AbstractContainerMenu {
}
}
- private static void dropOrPlaceInInventory(Player player, ItemStack stack) {
+ public static void dropOrPlaceInInventory(Player player, ItemStack stack) { // Lophine - better shulker box
boolean flag = !player.isAlive(); //player.isRemoved() && player.getRemovalReason() != Entity.RemovalReason.CHANGED_DIMENSION; // Luminol - Fix uncorrected death check of folia
boolean flag1 = player instanceof ServerPlayer serverPlayer && serverPlayer.hasDisconnected();
if (flag || flag1) {
diff --git a/net/minecraft/world/inventory/ShulkerBoxMenu.java b/net/minecraft/world/inventory/ShulkerBoxMenu.java
index 903025c659e6a9423224fe65973696405c69ec6a..545fc46416932db922dc1476b983ac8b9b9f3c7b 100644
--- a/net/minecraft/world/inventory/ShulkerBoxMenu.java
+++ b/net/minecraft/world/inventory/ShulkerBoxMenu.java
@@ -53,7 +53,7 @@ public class ShulkerBoxMenu extends AbstractContainerMenu {
@Override
public boolean stillValid(Player player) {
- if (!this.checkReachable) return true; // CraftBukkit
+ if (!this.checkReachable || player.shulkerOpen) return true; // CraftBukkit // Lophine - better shulker box
return this.container.stillValid(player);
}
diff --git a/net/minecraft/world/item/Item.java b/net/minecraft/world/item/Item.java
index c52fb17d1e496a91223b7387cacb128c1865caee..86d43c4e81dfe9775400e09ae706d4e9cc640bd8 100644
--- a/net/minecraft/world/item/Item.java
+++ b/net/minecraft/world/item/Item.java
@@ -187,6 +187,13 @@ public class Item implements FeatureElement, ItemLike {
player.startUsingItem(hand);
return InteractionResult.CONSUME;
} else {
+ // Lophine start - better shulker box
+ if (me.earthme.lophine.config.modules.misc.ContainerExpansionConfig.betterShulker
+ && player.isShiftKeyDown()
+ && me.earthme.lophine.utils.ShulkerBoxesUtil.checkIfCanOpen(itemInHand)) {
+ me.earthme.lophine.utils.ShulkerBoxesUtil.openShulkerBox(player, itemInHand, hand);
+ }
+ // Lophine end - better shulker box
return InteractionResult.PASS;
}
}
diff --git a/net/minecraft/world/item/component/ItemContainerContents.java b/net/minecraft/world/item/component/ItemContainerContents.java
index ecf794f94177fc7b6df483516d920719fbc6fa43..4fda59a4c17009a0009f3d3c13258f4ffcb300c3 100644
--- a/net/minecraft/world/item/component/ItemContainerContents.java
+++ b/net/minecraft/world/item/component/ItemContainerContents.java
@@ -33,7 +33,7 @@ public final class ItemContainerContents implements TooltipProvider {
public final NonNullList<ItemStack> items;
private final int hashCode;
- private ItemContainerContents(NonNullList<ItemStack> items) {
+ public ItemContainerContents(NonNullList<ItemStack> items) { // Lophine - better shulker box
if (items.size() > 256) {
throw new IllegalArgumentException("Got " + items.size() + " items, but maximum is 256");
} else {
diff --git a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
index 87ebdb6deb66662a38b3eec0dae27eaf859ecabb..e22114dbfbfd1619260a45929397c11c9356b8a0 100644
--- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
@@ -46,6 +46,11 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl
private ShulkerBoxBlockEntity.AnimationStatus animationStatus = ShulkerBoxBlockEntity.AnimationStatus.CLOSED;
private float progress;
private float progressOld;
+ // Lophine start - better shulker box
+ public boolean haveRealBlock = true;
+ public Player createPlayer;
+ public ItemStack item;
+ // Lophine end - better shulker box
@Nullable
private final DyeColor color;
@@ -236,6 +241,15 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl
return Component.translatable("container.shulkerBox");
}
+ // Lophine start - better shulker box
+ public void setItem(int index, ItemStack stack) {
+ super.setItem(index, stack);
+ if (!createPlayer.pendingClosingShulker && !haveRealBlock && me.earthme.lophine.config.modules.misc.ContainerExpansionConfig.betterShulker) {
+ me.earthme.lophine.utils.ShulkerBoxesUtil.shulkerBoxEntityCallBack(this);
+ }
+ }
+ // Lophine end - better shulker box
+
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
@@ -258,12 +272,12 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl
}
@Override
- protected NonNullList<ItemStack> getItems() {
+ public NonNullList<ItemStack> getItems() { // Lophine - better shulker box
return this.itemStacks;
}
@Override
- protected void setItems(NonNullList<ItemStack> items) {
+ public void setItems(NonNullList<ItemStack> items) { // Lophine - better shulker box
this.itemStacks = items;
}
@@ -1,40 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Thu, 12 Jun 2025 04:44:24 +0800
Subject: [PATCH] I18n support
diff --git a/net/minecraft/locale/Language.java b/net/minecraft/locale/Language.java
index 7b9e2a1a208b46a69c16e6afd8b502259893574f..e2a42996bb01c99fc71a5994e8eeb66ce836af34 100644
--- a/net/minecraft/locale/Language.java
+++ b/net/minecraft/locale/Language.java
@@ -31,7 +31,7 @@ public abstract class Language {
public static final String DEFAULT = "en_us";
private static volatile Language instance = loadDefault();
- private static Language loadDefault() {
+ public static Language loadDefault() { // Lophine - I18n support
DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource();
Map<String, String> map = new HashMap<>();
BiConsumer<String, String> biConsumer = map::put;
@@ -65,7 +65,7 @@ public abstract class Language {
};
}
- private static void parseTranslations(BiConsumer<String, String> output, String languagePath) {
+ public static void parseTranslations(BiConsumer<String, String> output, String languagePath) { // Lophine - I18n support
try (InputStream resourceAsStream = Language.class.getResourceAsStream(languagePath)) {
loadFromJson(resourceAsStream, output);
} catch (JsonParseException | IOException var7) {
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index ba0cdba050cd3bf9231e15dd9543d34f37d9dae5..ce7d70afa524e5b8ea6f9de75d34c8f1eaa6ad72 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -172,6 +172,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
// Paper end - initialize global and world-defaults configuration
me.earthme.lophine.config.LophineConfig.loadConfigFiles(); //Luminol - load config file // Lophine - load config file
+ me.earthme.lophine.utils.LocalLangUtil.init(); // Lophine - I18n support
if (false) this.server.spark.enableEarlyIfRequested(); // Paper - spark // Luminol - Force disable builtin spark
// Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save
if (this.convertOldUsers()) {
@@ -0,0 +1,18 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Thu, 12 Jun 2025 04:44:24 +0800
Subject: [PATCH] I18n support
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index ba0cdba050cd3bf9231e15dd9543d34f37d9dae5..33e7cb15ceafdb246bb9cc1a43d9968d3f28ca6b 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -172,6 +172,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
// Paper end - initialize global and world-defaults configuration
me.earthme.lophine.config.LophineConfig.loadConfigFiles(); //Luminol - load config file // Lophine - load config file
+ me.earthme.lophine.utils.ServerI18nUtil.init(); // Lophine - I18n support
if (false) this.server.spark.enableEarlyIfRequested(); // Paper - spark // Luminol - Force disable builtin spark
// Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save
if (this.convertOldUsers()) {
@@ -1,6 +1,6 @@
--- /dev/null
+++ b/src/main/java/me/earthme/lophine/config/modules/misc/ContainerExpansionConfig.java
@@ -1,0 +_,32 @@
@@ -1,0 +_,37 @@
+package me.earthme.lophine.config.modules.misc;
+
+import me.earthme.luminol.config.EnumConfigCategory;
@@ -23,6 +23,11 @@
+ range: 1~64""")
+ public static int shulkerCount = 1;
+
+ @ConfigInfo(baseName = "better_shulker_box", comments =
+ """
+ Enable sneak + use to open shulker box.""")
+ public static boolean betterShulker = false;
+
+ @Override
+ public EnumConfigCategory getCategory() {
+ return EnumConfigCategory.MISC;
@@ -7,8 +7,8 @@
+import me.earthme.luminol.config.IConfigModule;
+import me.earthme.luminol.config.flags.ConfigInfo;
+
+public class LanguageConfig implements IConfigModule {
+ @ConfigInfo(baseName = "lang" ,comments = """
+public class LanguageConfig implements IConfigModule {
+ @ConfigInfo(baseName = "lang", comments = """
+ Please use the key from https://minecraft.wiki/w/Language
+ Sample of format: en_us zh_cn zh_hk zh_tw""")
+ public static String lang = "en_us";
@@ -1,12 +1,11 @@
--- /dev/null
+++ b/src/main/java/me/earthme/lophine/config/modules/removed/RemovedConfig.java
@@ -1,0 +_,25 @@
@@ -1,0 +_,24 @@
+package me.earthme.lophine.config.modules.removed;
+
+import me.earthme.luminol.config.EnumConfigCategory;
+import me.earthme.luminol.config.IConfigModule;
+import me.earthme.luminol.config.flags.ConfigInfo;
+import me.earthme.luminol.config.flags.DoNotLoad;
+import me.earthme.luminol.config.flags.TransformedConfig;
+
+public class RemovedConfig implements IConfigModule {
@@ -1,270 +0,0 @@
--- /dev/null
+++ b/src/main/java/me/earthme/lophine/utils/LocalLangUtil.java
@@ -1,0 +_,267 @@
+package me.earthme.lophine.utils;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import net.minecraft.locale.DeprecatedTranslationsInfo;
+import net.minecraft.locale.Language;
+import net.minecraft.network.chat.FormattedText;
+import net.minecraft.network.chat.Style;
+import net.minecraft.util.FormattedCharSequence;
+import net.minecraft.util.StringDecomposer;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.logging.Logger;
+
+public class LocalLangUtil {
+ final static Logger logger = Logger.getLogger("LangLoader");
+ final static String VERSION = "1.21.5";
+ final static String basePath = "cache/lophine/" + VERSION + "/";
+ static String lang = me.earthme.lophine.config.modules.optimizations.LanguageConfig.lang;
+
+ public static void init() {
+ init(true);
+ }
+
+ public static void init(boolean init) {
+ if (Objects.equals(lang, "en_us")) return;
+ CompletableFuture.runAsync(() -> {
+ try {
+ String path = basePath + "lang/" + lang + ".json";
+ if (!Files.exists(Path.of(path))) {
+ downloadLangAndCheck();
+ }
+ try {
+ loadLocalLang(path, init);
+ } catch (JsonParseException e) {
+ cleanCache();
+ if (init) {
+ init(false);
+ }
+ }
+ } catch (Exception e) {
+ logger.severe(() -> "Async initialization failed: " + e.getMessage());
+ }
+ });
+ }
+
+ private static void downloadLangAndCheck() {
+ String path = basePath + VERSION + ".json";
+ JsonObject json;
+ if (Files.exists(Path.of(path))) {
+ try {
+ json = loadJson(path);
+ } catch (Exception e) {
+ logger.warning("Failed to load local JSON: " + e.getMessage());
+ json = download();
+ }
+ } else {
+ json = download();
+ }
+
+ if (json == null) {
+ logger.warning("Failed to load language metadata");
+ return;
+ }
+
+ try {
+ JsonObject assetIndex = json.getAsJsonObject("assetIndex");
+ String assetUrl = assetIndex.get("url").getAsString();
+ byte[] assetData = fetchAndSave(assetUrl, basePath + "resource.json");
+
+ JsonObject assets = JsonParser.parseString(new String(assetData)).getAsJsonObject();
+ JsonObject langEntry = assets.getAsJsonObject("objects")
+ .getAsJsonObject("minecraft/lang/" + lang + ".json");
+
+ String hash = langEntry.get("hash").getAsString();
+ if (hash == null || hash.length() < 2) {
+ throw new IllegalArgumentException("Invalid hash value");
+ }
+
+ downloadLang(hash);
+ } catch (Exception e) {
+ logger.warning("Asset processing failed: " + e.getMessage());
+ }
+ }
+
+ private static void downloadLang(String hash) {
+ String url = "https://resources.download.minecraft.net/"
+ + hash.substring(0, 2) + "/" + hash;
+
+ for (int i = 0; i < 3; i++) {
+ try {
+ fetchAndSave(url, basePath + "lang/" + lang + ".json");
+ return;
+ } catch (Exception e) {
+ if (i == 2) logger.warning("Final attempt failed: " + e.getMessage());
+ }
+ }
+ }
+
+ private static JsonObject download() {
+ String versionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
+ String targetVersionUrl = null;
+
+ // Phase 1: Fetch version manifest
+ for (int i = 0; i < 3; i++) {
+ try (InputStreamReader reader = new InputStreamReader(
+ new ByteArrayInputStream(fetch(versionManifestUrl)))) {
+ JsonObject manifest = JsonParser.parseReader(reader).getAsJsonObject();
+ for (JsonElement element : manifest.getAsJsonArray("versions")) {
+ JsonObject version = element.getAsJsonObject();
+ if (VERSION.equals(version.get("id").getAsString())) {
+ targetVersionUrl = version.get("url").getAsString();
+ break;
+ }
+ }
+ if (targetVersionUrl != null) break;
+ } catch (Exception e) {
+ logger.warning("Failed to fetch version manifest: " + e.getMessage());
+ }
+ }
+
+ if (targetVersionUrl == null) return null;
+
+ // Phase 2: Fetch version metadata
+ for (int i = 0; i < 3; i++) {
+ try (InputStreamReader reader = new InputStreamReader(
+ new ByteArrayInputStream(fetchAndSave(targetVersionUrl, basePath + VERSION + ".json")))) {
+ return JsonParser.parseReader(reader).getAsJsonObject();
+ } catch (Exception e) {
+ logger.warning("Failed to fetch version metadata: " + e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ public static byte[] fetch(String urlString) throws IOException {
+ HttpURLConnection conn = (HttpURLConnection) new URL(urlString).openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(10000);
+
+ try (InputStream stream = conn.getInputStream()) {
+ if (conn.getResponseCode() != 200) {
+ throw new IOException("HTTP error: " + conn.getResponseCode());
+ }
+ return stream.readAllBytes();
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ public static byte[] fetchAndSave(String url, String savePath) throws IOException {
+ byte[] data = fetch(url);
+ Path outputPath = Paths.get(savePath);
+ Files.createDirectories(outputPath.getParent());
+ Files.write(outputPath, data);
+ return data;
+ }
+
+ private static void cleanCache() {
+ Path cachePath = Paths.get(basePath);
+ try {
+ if (Files.exists(cachePath)) {
+ Files.walk(cachePath)
+ .sorted(Comparator.reverseOrder())
+ .forEach(path -> {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ logger.warning("Failed to delete: " + path + " - " + e.getMessage());
+ }
+ });
+ logger.info("Cache cleaned: " + cachePath);
+ } else {
+ logger.info("Cache directory does not exist: " + cachePath);
+ }
+ } catch (IOException e) {
+ logger.severe("Cache cleanup failed: " + e.getMessage());
+ }
+ }
+
+
+ public static JsonObject loadJson(String path) throws IOException {
+ byte[] data = Files.readAllBytes(Paths.get(path));
+ return JsonParser.parseString(new String(data)).getAsJsonObject();
+ }
+
+ public static void loadLocalLang(String lang, boolean init) throws IOException {
+ try {
+ Language.inject(load(lang));
+ } catch (Exception e) {
+ logger.warning("Failed to load language file for " + lang + "\n" + e);
+ logger.info("Load default en_us instead of local lang " + lang);
+ if (init) {
+ throw e;
+ } else {
+ Language.inject(Language.loadDefault());
+ }
+ }
+ }
+
+ private static Language load(String lang) throws IOException {
+ DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource();
+ Map<String, String> map = new HashMap<>();
+ BiConsumer<String, String> biConsumer = map::put;
+ Language.parseTranslations(biConsumer, "/assets/minecraft/lang/en_us.json");
+ parseTranslations(biConsumer, lang);
+ deprecatedTranslationsInfo.applyToMap(map);
+ final Map<String, String> map1 = Map.copyOf(map);
+ return new Language() {
+ @Override
+ public String getOrDefault(String key, String defaultValue) {
+ return map1.getOrDefault(key, defaultValue);
+ }
+
+ @Override
+ public boolean has(String id) {
+ return map1.containsKey(id);
+ }
+
+ @Override
+ public boolean isDefaultRightToLeft() {
+ return false;
+ }
+
+ @Override
+ public FormattedCharSequence getVisualOrder(FormattedText text) {
+ return sink -> text.visit(
+ (style, content) -> StringDecomposer.iterateFormatted(content, style, sink) ? Optional.empty() : FormattedText.STOP_ITERATION,
+ Style.EMPTY
+ )
+ .isPresent();
+ }
+ };
+ }
+
+ public static void parseTranslations(BiConsumer<String, String> output, String languagePath) throws IOException {
+ try {
+ Path filePath = Paths.get(languagePath);
+ try (InputStream fileStream = Files.newInputStream(filePath)) {
+ Language.loadFromJson(fileStream, output);
+ } catch (java.nio.file.NoSuchFileException fileEx) {
+ logger.warning("Language file not found in both locations: " + languagePath);
+ throw fileEx;
+ } catch (Exception fileEx) {
+ logger.warning("Failed to load from filesystem " + filePath + "\n" + fileEx);
+ throw fileEx;
+ }
+ } catch (Exception ep) {
+ logger.warning("Couldn't read strings from " + languagePath + "\n" + ep);
+ throw ep;
+ }
+ }
+}
@@ -0,0 +1,254 @@
--- /dev/null
+++ b/src/main/java/me/earthme/lophine/utils/ServerI18nUtil.java
@@ -1,0 +_,251 @@
+package me.earthme.lophine.utils;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import me.earthme.lophine.config.modules.optimizations.LanguageConfig;
+import net.minecraft.locale.DeprecatedTranslationsInfo;
+import net.minecraft.locale.Language;
+import net.minecraft.network.chat.FormattedText;
+import net.minecraft.network.chat.Style;
+import net.minecraft.util.FormattedCharSequence;
+import net.minecraft.util.StringDecomposer;
+import org.apache.commons.io.FileUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.logging.Logger;
+
+/*
+* authored by: Helvetica Volubi <suisuroru@blue-millennium.fun>
+* modified by: Lumine1909 <133463833+Lumine1909@users.noreply.github.com>
+* Some of diff form Leaves
+*/
+public class ServerI18nUtil {
+
+ private static final Logger logger = Logger.getLogger("LangLoader");
+ private static final String VERSION = "1.21.5";
+ private static final String BASE_PATH = "cache/lophine/" + VERSION + "/";
+ private static final String manifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
+ private static final String resourceBaseUrl = "https://resources.download.minecraft.net/";
+
+ private static String langPath;
+ private static String assetsPath;
+ private static String versionPath;
+ private static String manifestPath;
+ private static String langJsonPath;
+
+ public static void init() {
+ if (Objects.equals(LanguageConfig.lang, "en_us")) {
+ return;
+ }
+ langPath = BASE_PATH + "lang/" + LanguageConfig.lang + ".json";
+ assetsPath = BASE_PATH + "assets.json";
+ versionPath = BASE_PATH + VERSION + ".json";
+ manifestPath = BASE_PATH + "manifest.json";
+ langJsonPath = "minecraft/lang/" + LanguageConfig.lang + ".json";
+ logger.info("Starting load language: " + LanguageConfig.lang);
+ CompletableFuture.runAsync(() -> loadI18n(LanguageConfig.lang, 2));
+ }
+
+ private static void loadI18n(String lang, int retryTime) {
+ try {
+ if (!Files.exists(Path.of(langPath))) {
+ downloadLang(true);
+ }
+ Language.inject(createLangInstance());
+ logger.info("Successfully loaded language: " + lang);
+ } catch (Exception e) {
+ logger.warning("Failed to load language file for " + lang + "\n" + e);
+ if (retryTime > 0) {
+ cleanCache();
+ loadI18n(lang, retryTime - 1);
+ } else {
+ logger.severe("Failed to load for many times, use default lang \"en_us\" instead");
+ cleanCache();
+ }
+ }
+ }
+
+ private static void downloadLang(boolean fetchFromAssets) throws Exception {
+ JsonObject json;
+ if (!Files.exists(Path.of(assetsPath)) || (json = loadJson(assetsPath)) == null) {
+ if (fetchFromAssets) {
+ downloadAssets(true);
+ downloadLang(false);
+ }
+ return;
+ }
+
+ JsonObject langEntry = json.getAsJsonObject("objects").getAsJsonObject(langJsonPath);
+
+ String hash = langEntry.get("hash").getAsString();
+ if (hash == null || hash.length() < 2) {
+ throw new IllegalArgumentException("Invalid hash value");
+ }
+
+ String langUrl = resourceBaseUrl + hash.substring(0, 2) + "/" + hash;
+ fetchAndSave(langUrl, langPath);
+ }
+
+ private static void downloadAssets(boolean fetchFromVersion) throws Exception {
+ JsonObject json;
+ if (!Files.exists(Path.of(versionPath)) || (json = loadJson(versionPath)) == null) {
+ if (fetchFromVersion) {
+ downloadVersion(true);
+ downloadAssets(false);
+ }
+ return;
+ }
+
+ JsonObject assetIndex = json.getAsJsonObject("assetIndex");
+ String assetUrl = assetIndex.get("url").getAsString();
+ fetchAndSave(assetUrl, assetsPath);
+ }
+
+ private static void downloadVersion(boolean fetchFromManifest) throws Exception {
+ JsonObject json;
+ if (!Files.exists(Path.of(manifestPath)) || (json = loadJson(manifestPath)) == null) {
+ if (fetchFromManifest) {
+ fetchAndSave(manifestUrl, manifestPath);
+ downloadVersion(false);
+ }
+ return;
+ }
+
+ String versionUrl = null;
+ for (JsonElement element : json.getAsJsonArray("versions")) {
+ String id = element.getAsJsonObject().get("id").getAsString();
+ String url = element.getAsJsonObject().get("url").getAsString();
+ if (VERSION.equals(id)) {
+ versionUrl = url;
+ break;
+ }
+ }
+
+ if (versionUrl == null) {
+ throw new RuntimeException("Could not find version URL");
+ }
+
+ fetchAndSave(versionUrl, versionPath);
+ }
+
+ private static String createHttpResponse(String path) throws IOException, InterruptedException {
+ try {
+ HttpResponse<String> response;
+ try (HttpClient httpClient = HttpClient.newHttpClient()) {
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(path))
+ .build();
+
+ response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ }
+
+ int responseCode = response.statusCode();
+ if (responseCode != 200) {
+ logger.info("Unexpected response code: " + responseCode);
+ logger.info("Response body: " + response.body());
+ throw new UnsupportedEncodingException("Unexpected response code");
+ } else {
+ return response.body();
+ }
+ } catch (Exception e) {
+ logger.warning("Error in getting info: " + e.getMessage());
+ throw e;
+ }
+ }
+
+ private static byte[] fetch(String urlString) throws IOException, InterruptedException {
+ String ret = createHttpResponse(urlString);
+ return ret.getBytes();
+ }
+
+ private static void fetchAndSave(String url, String savePath) throws IOException, InterruptedException {
+ byte[] data = fetch(url);
+ Path outputPath = Path.of(savePath);
+ Files.createDirectories(outputPath.getParent());
+ Files.write(outputPath, data, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+ }
+
+ private static void cleanCache() {
+ try {
+ FileUtils.deleteDirectory(Path.of(BASE_PATH).toFile());
+ } catch (IOException e) {
+ logger.severe("Cache cleanup failed: " + e);
+ }
+ }
+
+ private static JsonObject loadJson(String path) {
+ try {
+ byte[] data = Files.readAllBytes(Paths.get(path));
+ return JsonParser.parseString(new String(data)).getAsJsonObject();
+ } catch (JsonSyntaxException e) {
+ logger.warning("Corrupt json file: " + e);
+ throw e;
+ } catch (Exception e) {
+ logger.warning("Failed to load local JSON: " + e);
+ return null;
+ }
+ }
+
+ private static Language createLangInstance() throws IOException {
+ DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource();
+ Map<String, String> map = new HashMap<>();
+ parseTranslations(map::put);
+ deprecatedTranslationsInfo.applyToMap(map);
+ final Map<String, String> map1 = Map.copyOf(map);
+ return new Language() {
+ @Override
+ public @NotNull String getOrDefault(@NotNull String key, @NotNull String defaultValue) {
+ return map1.getOrDefault(key, defaultValue);
+ }
+
+ @Override
+ public boolean has(@NotNull String id) {
+ return map1.containsKey(id);
+ }
+
+ @Override
+ public boolean isDefaultRightToLeft() {
+ return false;
+ }
+
+ @Override
+ public @NotNull FormattedCharSequence getVisualOrder(@NotNull FormattedText text) {
+ return sink -> text.visit(
+ (style, content) -> StringDecomposer.iterateFormatted(content, style, sink) ? Optional.empty() : FormattedText.STOP_ITERATION,
+ Style.EMPTY
+ )
+ .isPresent();
+ }
+ };
+ }
+
+ private static void parseTranslations(BiConsumer<String, String> output) throws IOException {
+ Path filePath = Path.of(langPath);
+ try (InputStream fileStream = Files.newInputStream(filePath)) {
+ Language.loadFromJson(fileStream, output);
+ } catch (NoSuchFileException noSuchFileException) {
+ logger.warning("Couldn't find language file: " + langPath);
+ throw noSuchFileException;
+ } catch (Exception e) {
+ logger.warning("Failed to load language from filesystem " + filePath + "\n" + e);
+ throw e;
+ }
+ }
+}
@@ -1,29 +1,41 @@
--- /dev/null
+++ b/src/main/java/me/earthme/lophine/utils/ShulkerBoxesUtil.java
@@ -1,0 +_,76 @@
@@ -1,0 +_,153 @@
+package me.earthme.lophine.utils;
+
+import me.earthme.lophine.config.modules.misc.ContainerExpansionConfig;
+import net.minecraft.core.NonNullList;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.stats.Stats;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.BlockItem;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.CustomData;
+import net.minecraft.world.item.component.ItemContainerContents;
+import net.minecraft.world.level.block.ShulkerBoxBlock;
+import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static net.minecraft.world.inventory.AbstractContainerMenu.dropOrPlaceInInventory;
+
+public class ShulkerBoxesUtil {
+ // Lophine - Stackable ShulkerBoxes
+ // Stackable ShulkerBoxes part
+ public static boolean shouldCheck() {
+ return ContainerExpansionConfig.shulkerCount > 1 && ContainerExpansionConfig.shulkerCount <= 64;
+ }
+
+ public static boolean checkShulkerBox(ItemStack itemStack) {
+ return shouldCheck() && itemStack.getItem() instanceof BlockItem b
+ && b.getBlock() instanceof ShulkerBoxBlock;
+ return shouldCheck() && checkIsShulkerBox(itemStack);
+ }
+
+ public static boolean checkIsShulkerBox(ItemStack itemStack) {
+ return itemStack.getItem() instanceof BlockItem b && b.getBlock() instanceof ShulkerBoxBlock;
+ }
+
+ public static int getItemMaxCount(ItemStack itemStack) {
@@ -76,4 +88,69 @@
+ }
+ return itemStack;
+ }
+
+ // Better ShulkerBox part
+ public static boolean checkIfCanOpen(ItemStack itemStack) {
+ return checkIsShulkerBox(itemStack) && itemStack.getCount() == 1;
+ }
+
+ public static void openShulkerBox(Player player, ItemStack itemInHand, InteractionHand hand) {
+ ShulkerBoxBlockEntity shulkerBoxEntity = new ShulkerBoxBlockEntity(
+ player.blockPosition(),
+ ((BlockItem) itemInHand.getItem()).getBlock().defaultBlockState()
+ );
+ ItemContainerContents container = itemInHand.getOrDefault(
+ DataComponents.CONTAINER,
+ ItemContainerContents.EMPTY
+ );
+
+ NonNullList<ItemStack> items = NonNullList.withSize(27, ItemStack.EMPTY);
+ for (int i = 0; i < container.items.size(); i++) {
+ items.set(i, container.items.get(i));
+ }
+
+ shulkerBoxEntity.setItems(items);
+
+ shulkerBoxEntity.setLevel(player.level());
+ shulkerBoxEntity.haveRealBlock = false;
+ shulkerBoxEntity.createPlayer = player;
+ player.shulkerHand = hand;
+ player.shulkerOpen = true;
+
+ if (player.openMenu(shulkerBoxEntity).isPresent()) {
+ player.awardStat(Stats.OPEN_SHULKER_BOX);
+ }
+ }
+
+ public static void shulkerBoxEntityCallBack(ShulkerBoxBlockEntity shulkerBoxEntity) {
+ Player player = shulkerBoxEntity.createPlayer;
+ if (player.shulkerOpen) {
+ InteractionHand hand = player.shulkerHand;
+ ItemStack currentItem = player.getItemInHand(hand);
+
+ currentItem.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(shulkerBoxEntity.getItems()));
+
+ player.setItemInHand(hand, currentItem);
+ }
+ }
+
+ public static void inventoryCallBack(boolean isMainHand, Player player) {
+ if (player.shulkerOpen) {
+ if (isMainHand == Objects.equals(player.shulkerHand, InteractionHand.MAIN_HAND)) {
+ player.pendingClosingShulker = true;
+ closeScreen(player);
+ player.pendingClosingShulker = false;
+ }
+ }
+ }
+
+ public static void closeScreen(Player player) {
+ if (player instanceof ServerPlayer) {
+ ItemStack stack = player.containerMenu.getCarried();
+ if (!stack.isEmpty()) {
+ dropOrPlaceInInventory(player, stack);
+ }
+ }
+ player.closeContainer();
+ }
+}