diff --git a/src/main/java/com/teammoeg/frostedheart/content/tips/Tip.java b/src/main/java/com/teammoeg/frostedheart/content/tips/Tip.java index 2dce44804..ea07e909c 100644 --- a/src/main/java/com/teammoeg/frostedheart/content/tips/Tip.java +++ b/src/main/java/com/teammoeg/frostedheart/content/tips/Tip.java @@ -7,6 +7,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import com.mojang.logging.LogUtils; +import com.teammoeg.frostedheart.util.client.ClientUtils; import com.teammoeg.frostedheart.util.client.FHColorHelper; import com.teammoeg.frostedheart.util.lang.Lang; import lombok.Getter; @@ -19,25 +20,28 @@ import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.contents.TranslatableContents; import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.util.Size2i; import org.slf4j.Logger; import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Optional; @Getter public class Tip { private static final Logger LOGGER = LogUtils.getLogger(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - public static final Tip EMPTY = Tip.builder("empty").error(ErrorType.EMPTY).build(); + public static final Tip EMPTY = Tip.builder("empty").error(ErrorType.OTHER, Lang.tips("error.not_exists").component()).build(); /** * 显示内容 @@ -57,6 +61,11 @@ public class Tip { */ @Nullable private final ResourceLocation image; + /** + * 图像的宽高 + */ + @Nullable + private final Size2i imageSize; /** * 是否始终显示 */ @@ -96,6 +105,7 @@ protected Tip(Tip.Builder builder) { this.category = builder.category; this.nextTip = builder.nextTip; this.image = builder.image; + this.imageSize = builder.imageSize; this.alwaysVisible = builder.alwaysVisible; this.onceOnly = builder.onceOnly; this.hide = builder.hide; @@ -106,15 +116,20 @@ protected Tip(Tip.Builder builder) { this.backgroundColor = builder.BGColor; } - public void saveAsFile() { + public boolean hasNext() { + return TipManager.INSTANCE.hasTip(nextTip); + } + + public boolean saveAsFile() { File file = new File(TipManager.TIP_PATH, this.id + ".json"); try (FileWriter writer = new FileWriter(file)) { String json = GSON.toJson(toJson()); writer.write(json); + return true; } catch (IOException e) { LOGGER.error("Unable to save file: '{}'", file, e); - Tip exception = Tip.builder("exception").error(ErrorType.SAVE, e).build(); - TipManager.INSTANCE.display().force(exception); + Tip.builder("exception").error(ErrorType.SAVE, e, Lang.str(file.getName())).forceDisplay(); + return false; } } @@ -152,7 +167,6 @@ public JsonObject toJson() { json.addProperty("onceOnly", onceOnly); json.addProperty("hide", hide); json.addProperty("pin", pin); - json.addProperty("temporary", temporary); json.addProperty("displayTime", displayTime); json.addProperty("fontColor", Integer.toHexString(fontColor).toUpperCase()); json.addProperty("backgroundColor", Integer.toHexString(backgroundColor).toUpperCase()); @@ -174,12 +188,12 @@ public static Tip.Builder builder(String id) { return new Builder(id); } - protected static Tip fromJsonFile(File filePath) { + public static Tip fromJsonFile(File filePath) { LOGGER.debug("Loading tip '{}'", filePath.getName()); Tip.Builder builder = builder("exception"); if (!filePath.exists()) { LOGGER.error("File does not exists '{}'", filePath); - builder.error(ErrorType.NOT_EXISTS); + builder.error(ErrorType.LOAD, Lang.str(filePath.toString()), Lang.tips("error.file_not_exists").component()); } else { try { String content = new String(Files.readAllBytes(Paths.get(String.valueOf(filePath)))); @@ -188,11 +202,11 @@ protected static Tip fromJsonFile(File filePath) { } catch (JsonSyntaxException e) { LOGGER.error("Invalid JSON format '{}'", filePath, e); - builder.error(ErrorType.INVALID, e); + builder.error(ErrorType.LOAD, e, Lang.str(builder.getId()), Lang.tips("error.invalid_json").component()); } catch (Exception e) { LOGGER.error("Unable to load file '{}'", filePath, e); - builder.error(ErrorType.LOAD, e); + builder.error(ErrorType.LOAD, e, Lang.str(builder.getId()), Lang.tips("error.other").component()); } } return builder.build(); @@ -205,6 +219,7 @@ public static class Builder { private String category = ""; private String nextTip = ""; private ResourceLocation image; + private Size2i imageSize; private boolean alwaysVisible; private boolean onceOnly; private boolean hide; @@ -214,8 +229,19 @@ public static class Builder { private int fontColor = FHColorHelper.CYAN; private int BGColor = FHColorHelper.BLACK; + private boolean editable = true; + public Builder(String id) { this.id = id; + setTemporary(); + } + + public void display() { + TipManager.INSTANCE.display().general(build()); + } + + public void forceDisplay() { + TipManager.INSTANCE.display().force(build()); } public Tip build() { @@ -223,169 +249,254 @@ public Tip build() { } public Builder nextTip(String next) { + if (!editable) return this; this.nextTip = next; return this; } + public Builder category(String category) { + if (!editable) return this; + this.category = category; + return this; + } + public Builder line(Component text) { + if (!editable) return this; this.contents.add(text); return this; } public Builder lines(Collection texts) { + if (!editable) return this; this.contents.addAll(texts); return this; } public Builder clearContents() { + if (!editable) return this; this.contents.clear(); return this; } public Builder image(ResourceLocation image) { + if (!editable) return this; this.image = image; + imageSize(image); return this; } public Builder alwaysVisible(boolean alwaysVisible) { + if (!editable) return this; this.alwaysVisible = alwaysVisible; return this; } + public Builder onceOnly(boolean onceOnly) { + if (!editable) return this; + this.onceOnly = onceOnly; + return this; + } + + public Builder hide(boolean hide) { + if (!editable) return this; + this.hide = hide; + return this; + } + public Builder pin(boolean pin) { + if (!editable) return this; this.pin = pin; return this; } public Builder setTemporary() { + if (!editable) return this; this.temporary = true; return this; } - public Builder color(int fontColor, int BGColor) { + public Builder fontColor(int fontColor) { + if (!editable) return this; this.fontColor = fontColor; - this.BGColor = BGColor; return this; } - public Builder displayTime(int time) { - this.displayTime = time; + public Builder BGColor(int BGColor) { + if (!editable) return this; + this.BGColor = BGColor; return this; } - public Builder error(ErrorType type, Component... descriptions) { - return clearContents() - .line(Lang.tips("error." + type.key).component()) - .lines(Arrays.asList(descriptions)) - .line(Lang.tips("error.desc").component()) - .color(FHColorHelper.RED, FHColorHelper.BLACK) - .alwaysVisible(true) - .setTemporary() - .pin(true); + public Builder color(int fontColor, int BGColor) { + if (!editable) return this; + this.fontColor = fontColor; + this.BGColor = BGColor; + return this; } - public Builder error(ErrorType type, Exception exception, Component... descriptions) { - return error(type, descriptions) - .line(Lang.str(exception.getMessage())); + public Builder displayTime(int time) { + if (!editable) return this; + this.displayTime = time; + return this; } public Builder copy(Tip source) { + if (!editable) return this; + this.contents.addAll(source.contents); this.category = source.category; this.nextTip = source.nextTip; this.image = source.image; + this.imageSize = source.imageSize; this.alwaysVisible = source.alwaysVisible; this.onceOnly = source.onceOnly; this.hide = source.hide; this.pin = source.pin; - this.temporary = source.temporary; + this.temporary = true; this.displayTime = source.displayTime; this.fontColor = source.fontColor; this.BGColor = source.backgroundColor; return this; } + public Builder fromNBT(CompoundTag nbt) { + if (!editable) return this; + + if (nbt != null) { + setTemporary(); + this.id = nbt.getString("id"); + category(nbt.getString("category")); + nextTip(nbt.getString("nextTip")); + alwaysVisible(nbt.getBoolean("alwaysVisible")); + onceOnly(nbt.getBoolean("onceOnly")); + hide(nbt.getBoolean("hide")); + pin(nbt.getBoolean("pin")); + displayTime(nbt.getInt("displayTime")); + color(nbt.getInt("fontColor"), nbt.getInt("backgroundColor")); + + String location = nbt.getString("image"); + if (!location.isBlank()) image(ResourceLocation.tryParse(location)); + + var contents = nbt.getList("contents", Tag.TAG_STRING); + var list = contents.stream().map(tag -> Lang.translateOrElseStr(tag.getAsString())).toList(); + this.contents.addAll(list); + } + if (id.isBlank()) { + error(ErrorType.OTHER, Lang.str("NBT does not contain tip")); + } + return this; + } + public Builder fromJson(JsonObject json) { - if (json.has("category" )) this.category = json.get("category").getAsString(); - if (json.has("next" )) this.nextTip = json.get("next").getAsString(); - if (json.has("alwaysVisible" )) this.alwaysVisible = json.get("alwaysVisible").getAsBoolean(); - if (json.has("onceOnly" )) this.onceOnly = json.get("onceOnly").getAsBoolean(); - if (json.has("hide" )) this.hide = json.get("hide").getAsBoolean(); - if (json.has("pin" )) this.pin = json.get("pin").getAsBoolean(); - if (json.has("visibleTime" )) this.displayTime = Math.max(json.get("visibleTime").getAsInt(), 0); - if (json.has("fontColor" )) this.fontColor = tryGetColorOrElse(json, "fontColor", FHColorHelper.CYAN); - if (json.has("backgroundColor")) this.BGColor = tryGetColorOrElse(json, "backgroundColor", FHColorHelper.BLACK); + if (!editable) return this; + if (json.has("id")) { - this.id = json.get("id").getAsString(); + String s = json.get("id").getAsString(); + if (s.isBlank()) { + error(ErrorType.LOAD, Lang.str(getId()), Lang.tips("error.no_id").component()); + id = "exception"; + return this; + } + id = s; } else { - error(ErrorType.LOAD, Lang.str("This tip cannot be loaded because there is no id in its file")); + error(ErrorType.LOAD, Lang.str(getId()), Lang.tips("error.no_id").component()); id = "exception"; return this; } - if (json.has("image")) { - ResourceLocation image = ResourceLocation.tryParse(json.get("image").getAsString()); - if (image != null) { - this.image = image; - } else { - error(ErrorType.LOAD, Lang.str("The image ResourceLocation is invalid")); - return this; - } - } + if (json.has("contents")) { JsonArray jsonContents = json.getAsJsonArray("contents"); - for (int i = 0; i < jsonContents.size(); i++) { - String s = jsonContents.get(i).getAsString(); - this.contents.add(Lang.translateOrElseStr(s)); + if (jsonContents != null) { + for (int i = 0; i < jsonContents.size(); i++) { + String s = jsonContents.get(i).getAsString(); + line(Lang.translateOrElseStr(s)); + } } - if (this.contents.isEmpty()) { - error(ErrorType.EMPTY); - return this; + } + if (this.contents.isEmpty()) { + error(ErrorType.LOAD, Lang.str(getId()), Lang.tips("error.empty").component()); + return this; + } + + if (json.has("image")) { + String location = json.get("image").getAsString(); + if (!location.isBlank()) { + ResourceLocation image = ResourceLocation.tryParse(location); + if (image != null) { + image(image); + } else { + error(ErrorType.LOAD, Lang.str(getId()), Lang.tips("error.invalid_image", location).component()); + return this; + } } } + if (json.has("category" )) category (json.get("category").getAsString()); + if (json.has("next" )) nextTip (json.get("next").getAsString()); + if (json.has("alwaysVisible" )) alwaysVisible(json.get("alwaysVisible").getAsBoolean()); + if (json.has("onceOnly" )) onceOnly (json.get("onceOnly").getAsBoolean()); + if (json.has("hide" )) hide (json.get("hide").getAsBoolean()); + if (json.has("pin" )) pin (json.get("pin").getAsBoolean()); + if (json.has("visibleTime" )) displayTime (Math.max(json.get("visibleTime").getAsInt(), 0)); + if (json.has("fontColor" )) fontColor (getColorOrElse(json, "fontColor", FHColorHelper.CYAN)); + if (json.has("backgroundColor")) BGColor (getColorOrElse(json, "backgroundColor", FHColorHelper.BLACK)); + + temporary = false; return this; } - private int tryGetColorOrElse(JsonObject json, String name, int defColor) { - try { - return Integer.parseUnsignedInt(json.get(name).getAsString(), 16); - } catch (NumberFormatException e) { - error(ErrorType.LOAD, e, Lang.str("'" + name + "' is not a valid hexadecimal number")); - return defColor; - } + public Builder error(ErrorType type, Component... descriptions) { + clearContents() + .line(Lang.tips("error." + type.key).component()) + .lines(Arrays.asList(descriptions)) + .line(Lang.tips("error.desc").component()) + .color(FHColorHelper.RED, FHColorHelper.BLACK) + .alwaysVisible(true) + .setTemporary() + .pin(true); + this.editable = false; + return this; } - public Builder fromNBT(CompoundTag nbt) { - if (nbt != null) { - this.id = nbt.getString("id"); - this.category = nbt.getString("category"); - this.nextTip = nbt.getString("nextTip"); - this.image = ResourceLocation.tryParse(nbt.getString("image")); - this.alwaysVisible = nbt.getBoolean("alwaysVisible"); - this.onceOnly = nbt.getBoolean("onceOnly"); - this.hide = nbt.getBoolean("hide"); - this.pin = nbt.getBoolean("pin"); - this.temporary = nbt.getBoolean("temporary"); - this.displayTime = nbt.getInt("displayTime"); - this.fontColor = nbt.getInt("fontColor"); - this.BGColor = nbt.getInt("backgroundColor"); + public Builder error(ErrorType type, Exception exception, Component... descriptions) { + return error(type, descriptions) + .line(Lang.str(exception.getMessage())); + } - var contents = nbt.getList("contents", Tag.TAG_STRING); - var list = contents.stream().map(tag -> Lang.translateOrElseStr(tag.getAsString())).toList(); - this.contents.addAll(list); + private void imageSize(ResourceLocation location) { + var resource = ClientUtils.mc().getResourceManager().getResource(location); + if (resource.isPresent()) { + try (InputStream stream = resource.get().open()) { + BufferedImage image= ImageIO.read(stream); + Size2i size = new Size2i(image.getWidth(), image.getHeight()); + if (size.width != 0 || size.height != 0) { + this.imageSize = size; + return; + } + } catch (IOException e) { + LOGGER.error("Invalid texture resource location {}", location, e); + error(ErrorType.LOAD, e, Lang.tips("error.invalid_image").component()); + } } - return this; + this.image = null; + error(ErrorType.LOAD, Lang.tips("error.invalid_image").component()); } + private int getColorOrElse(JsonObject json, String name, int defColor) { + try { + return Integer.parseUnsignedInt(json.get(name).getAsString(), 16); + } catch (NumberFormatException e) { + line(Lang.tips("error.invalid_digit", name).component()); + return defColor; + } + } } public enum ErrorType { OTHER("other"), LOAD("load"), SAVE("save"), - EMPTY("empty"), - INVALID("invalid"), - NOT_EXISTS("not_exists"); + DISPLAY("display"); final String key; diff --git a/src/main/java/com/teammoeg/frostedheart/content/tips/TipManager.java b/src/main/java/com/teammoeg/frostedheart/content/tips/TipManager.java index 5398ad443..cfc39aaf5 100644 --- a/src/main/java/com/teammoeg/frostedheart/content/tips/TipManager.java +++ b/src/main/java/com/teammoeg/frostedheart/content/tips/TipManager.java @@ -20,7 +20,6 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -75,14 +74,14 @@ public Tip getTip(String id) { if (tip != null) { return tip; } - return Tip.builder(id).error(Tip.ErrorType.NOT_EXISTS, Lang.str(TIP_PATH.toString() + "\\" + id + ".json")).build(); + return Tip.builder(id).error(Tip.ErrorType.OTHER, Lang.str(id), Lang.tips("error.tip_not_exists").component()).build(); } /** - * 返回对应的 tip 实例是否存在 + * 对应的 tip 是否存在 */ public boolean hasTip(String id) { - return loadedTips.containsKey(id); + return id != null && loadedTips.containsKey(id); } /** @@ -98,17 +97,25 @@ public void loadFromFile() { File[] general = TIP_PATH.listFiles(); if (general != null) files.addAll(List.of(general)); if (!files.isEmpty()) { + int sum = 0; for (File tipFile : files) { Tip tip = Tip.fromJsonFile(tipFile); - loadedTips.put(tip.getId(), tip); + if (loadedTips.containsKey(tip.getId())) { + // 重复id + Tip d = Tip.builder("duplicate").error(Tip.ErrorType.LOAD, Lang.str(tip.getId()), Lang.tips("error.load.duplicate_id").component()).build(); + display.force(d); + } else { + loadedTips.put(tip.getId(), tip); + sum++; + } } - state.loadFromFile(); + LOGGER.debug("{} tips loaded", sum); } - LOGGER.debug("Loaded {} tips", files.size()); + state.loadFromFile(); } - private void displayException(Tip.ErrorType type, Exception e) { - Tip exception = Tip.builder("exception").error(type, e).build(); + private void displayException(Tip.ErrorType type, String id, Exception e) { + Tip exception = Tip.builder("exception").error(type, e, Lang.str(id)).build(); display.force(exception); } @@ -140,18 +147,22 @@ public void general(Tip tip) { // 更改非临时 tip 的状态 if (!tip.isTemporary()) { -// if (tip.id.startsWith("*custom*")) { -// TipStateManager.manager.unlockCustom(tip); -// } manager.state.setLockState(tip, true); } - if (tip.isPin()) { + if (tip.isPin() && !TipRenderer.TIP_QUEUE.isEmpty()) { + Tip last = TipRenderer.TIP_QUEUE.get(0); TipRenderer.removeCurrent(); + TipRenderer.TIP_QUEUE.add(0, last); TipRenderer.TIP_QUEUE.add(0, tip); } else { TipRenderer.TIP_QUEUE.add(tip); } + + // 添加下一个tip + if (!tip.getNextTip().isEmpty()) { + general(tip.getNextTip()); + } } /** @@ -165,8 +176,14 @@ public void force(String id) { * 无视 tip 的状态,在渲染队列中强制添加此 tip */ public void force(Tip tip) { + if (!tip.isTemporary()) { + manager.state.setLockState(tip, true); + } + if (tip.isPin()) { + Tip last = TipRenderer.TIP_QUEUE.get(0); TipRenderer.removeCurrent(); + TipRenderer.TIP_QUEUE.add(0, last); TipRenderer.TIP_QUEUE.add(0, tip); } else { TipRenderer.TIP_QUEUE.add(tip); @@ -177,12 +194,14 @@ public void force(Tip tip) { * 使 tip 永久显示,即 {@code alwaysVisible = true} */ public void alwaysVisible(Tip tip) { + if (TipRenderer.TIP_QUEUE.isEmpty()) return; + var list = TipRenderer.TIP_QUEUE; if (list.size() <= 1 || list.get(0) == tip) return; for (int i = 0; i < list.size(); i++) { Tip t = list.get(i); if (t == tip) { - Tip clone = Tip.builder("").copy(t).alwaysVisible(true).build(); + Tip clone = Tip.builder("copy").copy(t).alwaysVisible(true).build(); TipRenderer.TIP_QUEUE.set(i, clone); return; } @@ -192,15 +211,14 @@ public void alwaysVisible(Tip tip) { /** * 置顶 tip */ - public void pin(String id) { + public void pin(Tip tip) { var list = TipRenderer.TIP_QUEUE; - if (list.size() <= 1 || list.get(0).getId().equals(id)) return; - Iterator iterator = list.iterator(); - while (iterator.hasNext()) { - Tip tip = iterator.next(); - if (tip.getId().equals(id)) { - iterator.remove(); - list.add(0, tip); + if (list.size() <= 1 || list.get(0) == tip) return; + + for (Tip t : list) { + if (t == tip) { + TipRenderer.TIP_QUEUE.remove(t); + force(tip); return; } } @@ -244,7 +262,7 @@ protected void loadFromFile() { // 文件存在但是无法正确读取 if (TIP_STATE_FILE.exists()) { String message = "The file '" + TIP_STATE_FILE + "' already exists but cannot be read correctly, it may be corrupted"; - manager.displayException(Tip.ErrorType.LOAD, new Exception(message)); + manager.displayException(Tip.ErrorType.LOAD, "tip_states.json", new Exception(message)); LOGGER.warn(message); } return; @@ -254,7 +272,7 @@ protected void loadFromFile() { .collect(Collectors.toMap(state -> manager.getTip(state.id), s -> s))); } catch (IOException e) { LOGGER.error("Unable to load file: '{}'", TIP_STATE_FILE, e); - manager.displayException(Tip.ErrorType.LOAD, e); + manager.displayException(Tip.ErrorType.LOAD, "tip_states.json", e); } } @@ -267,7 +285,7 @@ public void saveToFile() { writer.write(json); } catch (IOException e) { LOGGER.error("Unable to save file: '{}'", TIP_STATE_FILE, e); - manager.displayException(Tip.ErrorType.SAVE, e); + manager.displayException(Tip.ErrorType.SAVE, "tip_states.json", e); } } diff --git a/src/main/java/com/teammoeg/frostedheart/content/tips/TipRenderer.java b/src/main/java/com/teammoeg/frostedheart/content/tips/TipRenderer.java index 249d15938..1c4d714ef 100644 --- a/src/main/java/com/teammoeg/frostedheart/content/tips/TipRenderer.java +++ b/src/main/java/com/teammoeg/frostedheart/content/tips/TipRenderer.java @@ -1,6 +1,5 @@ package com.teammoeg.frostedheart.content.tips; -import com.teammoeg.frostedheart.FHMain; import com.teammoeg.frostedheart.content.tips.client.gui.widget.TipWidget; import com.teammoeg.frostedheart.infrastructure.config.FHConfig; import com.teammoeg.frostedheart.util.client.ClientUtils; @@ -46,7 +45,7 @@ public static boolean isTipRendering() { */ public static void removeCurrent() { if (!TIP_QUEUE.isEmpty()) TIP_QUEUE.remove(0); - TipWidget.INSTANCE.setState(TipWidget.State.FADING_OUT); + TipWidget.INSTANCE.close(); } @SubscribeEvent @@ -120,12 +119,12 @@ public static void onGuiRender(ScreenEvent.Render.Post event) { } private static void update() { - if (TipWidget.INSTANCE.getState() == TipWidget.State.IDLE) { + if (!isTipRendering()) { TIP_QUEUE.remove(TipWidget.INSTANCE.lastTip); TipWidget.INSTANCE.lastTip = null; // 切换下一个 if (!TIP_QUEUE.isEmpty()) { - TipWidget.INSTANCE.setTip(TIP_QUEUE.get(0)); + TipWidget.INSTANCE.tip = TIP_QUEUE.get(0); } } } diff --git a/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/DebugScreen.java b/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/DebugScreen.java index af9ba15d0..82e6af571 100644 --- a/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/DebugScreen.java +++ b/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/DebugScreen.java @@ -1,6 +1,7 @@ package com.teammoeg.frostedheart.content.tips.client.gui; import com.teammoeg.frostedheart.FrostedHud; +import com.teammoeg.frostedheart.content.tips.Tip; import com.teammoeg.frostedheart.content.tips.TipManager; import com.teammoeg.frostedheart.content.tips.client.gui.widget.IconButton; import com.teammoeg.frostedheart.content.waypoint.ClientWaypointManager; @@ -9,6 +10,7 @@ import com.teammoeg.frostedheart.content.waypoint.waypoints.Waypoint; import com.teammoeg.frostedheart.util.client.ClientUtils; import com.teammoeg.frostedheart.util.client.FHColorHelper; +import com.teammoeg.frostedheart.util.lang.Lang; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Renderable; @@ -32,6 +34,8 @@ public DebugScreen() { @Override public void init() { + buttons.clear(); + addButton(IconButton.Icon.CROSS, FHColorHelper.CYAN, "Clear Tip Render Queue", (b) -> TipManager.INSTANCE.display().clearRenderQueue() ); @@ -72,10 +76,21 @@ public void init() { addButton(IconButton.Icon.TRADE, FHColorHelper.CYAN, "Toggle Debug Overlay", (b) -> FrostedHud.renderDebugOverlay = !FrostedHud.renderDebugOverlay ); + addButton(IconButton.Icon.LEAVE, FHColorHelper.CYAN, "Do Something", (b) -> { + String message = debug(); + ClientUtils.getPlayer().sendSystemMessage(Lang.str(message)); + }); super.init(); // addRenderableWidget(new TextLabelWidget(10, 10, 50, 50, Component.literal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), ClientUtils.font())); } + // 方便热重载debug + private String debug() { + Tip tip = Tip.builder("test").line(Lang.str("test")).line(Lang.str("aaaaaaaaaaaaa")).nextTip("default").build(); + TipManager.INSTANCE.display().general(tip); + return tip.getNextTip(); + } + public void addButton(IconButton.Icon icon, int color, String message, Button.OnPress onPress) { IconButton button = new IconButton(0, 0, icon, color, Component.literal(message), onPress); buttons.add(button); diff --git a/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/widget/TipWidget.java b/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/widget/TipWidget.java index 165a0ce0c..f1c7aaf08 100644 --- a/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/widget/TipWidget.java +++ b/src/main/java/com/teammoeg/frostedheart/content/tips/client/gui/widget/TipWidget.java @@ -8,43 +8,33 @@ import com.teammoeg.frostedheart.util.client.FHGuiHelper; import com.teammoeg.frostedheart.util.lang.Lang; import lombok.Getter; -import lombok.Setter; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.renderer.Rect2i; -import net.minecraft.client.sounds.SoundManager; import net.minecraft.network.chat.Component; import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.Mth; +import net.minecraftforge.common.util.Size2i; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.List; public class TipWidget extends AbstractWidget { - private static final String ANIMATION_PROGRESS_NAME = TipWidget.class.getSimpleName() + "_progress"; - private static final String ANIMATION_FADE_NAME = TipWidget.class.getSimpleName() + "_fade"; - private static final int FADE_ANIMATION_TIME_LENGTH = 400; - private static final int BACKGROUND_BORDER = 4; - private static final int FADE_OFFSET = 16; - private static final int LINE_SPACE = 12; - private static final int MIN_WIDTH = 160; - private static final int MAX_WIDTH = 360; public final IconButton closeButton; public final IconButton pinButton; public Tip lastTip; - @Setter - @Getter @Nullable - private Tip tip; + public Tip tip; @Getter - @Setter private State state; - private boolean alwaysVisibleOverride; @Getter private float progress; + private final RenderContext context; + private boolean alwaysVisibleOverride; /** * TipWidget实例 @@ -54,13 +44,14 @@ public class TipWidget extends AbstractWidget { private TipWidget() { super(0, 0, 0, 0, Component.literal("tip")); this.closeButton = new IconButton(0, 0, IconButton.Icon.CROSS, FHColorHelper.CYAN, Lang.gui("close").component(), - b -> state = State.FADING_OUT + b -> close() ); this.pinButton = new IconButton(0, 0, IconButton.Icon.LOCK, FHColorHelper.CYAN, Lang.gui("pin").component(), b -> this.alwaysVisibleOverride = true ); this.closeButton.visible = false; this.pinButton.visible = false; + this.context = new RenderContext(); } @Override @@ -72,21 +63,21 @@ public void renderWidget(@NotNull GuiGraphics graphics, int mouseX, int mouseY, switch (state) { case IDLE -> state = State.FADING_IN; case FADING_IN -> { - float f = AnimationUtil.fadeIn(FADE_ANIMATION_TIME_LENGTH, ANIMATION_FADE_NAME, false); + float f = AnimationUtil.fadeIn(RenderContext.FADE_ANIM_LENGTH, RenderContext.FADE_ANIM_NAME, false); render(graphics, mouseX, mouseY, partialTick, f); if (f == 1F) { state = isAlwaysVisible() ? State.DONE : State.PROGRESSING; - AnimationUtil.remove(ANIMATION_FADE_NAME); + AnimationUtil.remove(RenderContext.FADE_ANIM_NAME); } } case PROGRESSING -> { if (isAlwaysVisible()) state = State.DONE; - progress = AnimationUtil.progress(tip.getDisplayTime(), ANIMATION_PROGRESS_NAME, false); + progress = AnimationUtil.progress(tip.getDisplayTime(), RenderContext.PROGRESS_ANIM_NAME, false); render(graphics, mouseX, mouseY, partialTick, 1); if (progress == 1F) state = State.FADING_OUT; } case FADING_OUT -> { - float f = 1F - AnimationUtil.fadeIn(FADE_ANIMATION_TIME_LENGTH, ANIMATION_FADE_NAME, false); + float f = 1F - AnimationUtil.fadeIn(RenderContext.FADE_ANIM_LENGTH, RenderContext.FADE_ANIM_NAME, false); render(graphics, mouseX, mouseY, partialTick, f); if (f == 0F) resetState(); } @@ -100,78 +91,76 @@ public void renderWidget(@NotNull GuiGraphics graphics, int mouseX, int mouseY, private void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick, float progress) { if (tip == null || tip.getContents().isEmpty()) return; - var contents = tip.getContents(); - int fontColor = FHColorHelper.setAlpha(tip.getFontColor(), Math.max(progress, 0.1F)); - int backgroundColor = FHColorHelper.setAlpha(tip.getBackgroundColor(), (isGuiOpened() ? 0.8F : 0.5F) * progress); - int screenW = ClientUtils.screenWidth(); - int screenH = ClientUtils.screenHeight(); - setWidth(Mth.clamp((int)(screenW * 0.3F), MIN_WIDTH, MAX_WIDTH)); - - // 文本 - var titleLines = ClientUtils.font().split(contents.get(0), super.getWidth()-24); - var contentLines = new ArrayList(); - if (contents.size() > 1) - for (int i = 1; i < contents.size(); i++) - contentLines.addAll(ClientUtils.font().split(contents.get(i), super.getWidth()-24)); - setHeight((titleLines.size() + contentLines.size()) * LINE_SPACE); - - // 跟随视角晃动 - float yaw = 0; - float pitch = 0; - Minecraft MC = ClientUtils.mc(); - if (MC.player != null) { - if (MC.isPaused()) { - yaw = (MC.player.getViewYRot(MC.getFrameTime()) - MC.player.yBob) * -0.1F; - pitch = (MC.player.getViewXRot(MC.getFrameTime()) - MC.player.xBob) * -0.1F; - } else { - yaw = (MC.player.getViewYRot(MC.getFrameTime()) - - Mth.lerp(MC.getFrameTime(), MC.player.yBobO, MC.player.yBob)) * -0.1F; - pitch = (MC.player.getViewXRot(MC.getFrameTime()) - - Mth.lerp(MC.getFrameTime(), MC.player.xBobO, MC.player.xBob)) * -0.1F; - } - } + + context.update(tip); + setWidth(context.size.width); + setHeight(context.size.height); + setX(context.screenSize.width - super.getWidth() - 4 - RenderContext.BG_BORDER + (int) context.pYaw); + setY((int)(context.screenSize.height * 0.3F) + (int) context.pPitch); + setAlpha(progress); + graphics.setColor(1, 1, 1, alpha); - // 渲染 - setPosition((screenW - width - 8 + (int)yaw), ((int)(screenH * 0.3F) + (int)pitch)); + int border = RenderContext.BG_BORDER; PoseStack pose = graphics.pose(); pose.pushPose(); - pose.translate((yaw - (int)yaw), (pitch - (int)pitch), 800); + pose.translate((context.pYaw - (int) context.pYaw), (context.pPitch - (int) context.pPitch), 800); // 背景 - graphics.fill(getX()-BACKGROUND_BORDER, getY()-BACKGROUND_BORDER, getX()+width+BACKGROUND_BORDER, getY()+getHeight() + (contentLines.isEmpty() ? 1 : 6), backgroundColor); + graphics.fill( + getX() - border, + getY() - border, + getX() + super.getWidth() + border, + getY() + getHeight() + (!context.contentLines.isEmpty() || context.hasImage ? 2 + border : 1), + context.BGColor); // 进度条 if (state != State.FADING_OUT) { - int y = getY() + (titleLines.size() * LINE_SPACE); + int y = getY() + (context.titleLines.size() * RenderContext.LINE_SPACE); pose.pushPose(); - pose.translate(getX()-BACKGROUND_BORDER, y, 0); + pose.translate(getX()- border, y, 0); if (state == State.PROGRESSING) { // 更平滑的进度条效果 - pose.scale(1 - AnimationUtil.getProgress(ANIMATION_PROGRESS_NAME), 1, 1); + pose.scale(1 - AnimationUtil.getProgress(RenderContext.PROGRESS_ANIM_NAME), 1, 1); } - graphics.fill(0, 0, super.getWidth()+(BACKGROUND_BORDER*2), 1, fontColor); + graphics.fill(0, 0, super.getWidth()+(border *2), 1, context.fontColor); pose.popPose(); } - // 标题和内容 - FHGuiHelper.drawStrings(graphics, ClientUtils.font(), titleLines, getX(), getY(), fontColor, LINE_SPACE, false); - FHGuiHelper.drawStrings(graphics, ClientUtils.font(), contentLines, getX(), getY()+6 + (titleLines.size() * LINE_SPACE), fontColor, LINE_SPACE, false); + // 图片 + if (context.hasImage) { + FHGuiHelper.bindTexture(tip.getImage()); + FHGuiHelper.blitColored( + pose, + getX() + (super.getWidth() / 2) - (context.imageSize.width / 2), + getY() + border + (context.titleLines.size() + context.contentLines.size()) * RenderContext.LINE_SPACE, + context.imageSize.width, context.imageSize.height, + 0, 0, + context.imageSize.width, context.imageSize.height, + context.imageSize.width, context.imageSize.height, + 0xFFFFFFFF, alpha); + } // 按钮 pose.translate(0, 0, 100); - closeButton.color = fontColor; + closeButton.color = context.fontColor; closeButton.visible = true; closeButton.setPosition(getX() + super.getWidth() - 10, getY()); closeButton.render(graphics, mouseX, mouseY, partialTick); if (isGuiOpened() && !isAlwaysVisible() && state != State.FADING_OUT) { - pinButton.color = fontColor; + pinButton.color = context.fontColor; pinButton.visible = true; pinButton.setPosition(getX() + super.getWidth() - 22, getY()); pinButton.render(graphics, mouseX, mouseY, partialTick); } else { pinButton.visible = false; } + + // 标题和内容 + FHGuiHelper.drawStrings(graphics, ClientUtils.font(), context.titleLines, getX(), getY(), context.fontColor, RenderContext.LINE_SPACE, false); + FHGuiHelper.drawStrings(graphics, ClientUtils.font(), context.contentLines, getX(), getY()+6 + (context.titleLines.size() * RenderContext.LINE_SPACE), context.fontColor, RenderContext.LINE_SPACE, false); + pose.popPose(); + graphics.setColor(1, 1, 1, 1); } /** @@ -181,6 +170,10 @@ public boolean isGuiOpened() { return ClientUtils.mc().screen != null; } + public void close() { + state = State.FADING_OUT; + } + /** * 重置状态 */ @@ -188,16 +181,17 @@ public void resetState() { lastTip = tip; tip = null; progress = 0; + context.clear(); state = State.IDLE; alwaysVisibleOverride = false; pinButton.visible = false; closeButton.visible = false; pinButton.setFocused(false); closeButton.setFocused(false); + AnimationUtil.remove(RenderContext.FADE_ANIM_NAME); + AnimationUtil.remove(RenderContext.PROGRESS_ANIM_NAME); // 将位置设置到屏幕外避免影响屏幕内的元素 setPosition(ClientUtils.screenWidth(), ClientUtils.screenHeight()); - AnimationUtil.remove(ANIMATION_FADE_NAME); - AnimationUtil.remove(ANIMATION_PROGRESS_NAME); } /** @@ -214,9 +208,9 @@ public Rect2i getRect() { @Override public int getX() { if (state == State.FADING_IN) - return super.getX() - FADE_OFFSET + (int)(FADE_OFFSET * AnimationUtil.getFadeIn(ANIMATION_FADE_NAME)); + return super.getX() - RenderContext.FADE_OFFSET + (int)(RenderContext.FADE_OFFSET * AnimationUtil.getFadeIn(RenderContext.FADE_ANIM_NAME)); if (state == State.FADING_OUT) - return super.getX() - (int)(FADE_OFFSET * AnimationUtil.getFadeOut(ANIMATION_FADE_NAME)); + return super.getX() - (int)(RenderContext.FADE_OFFSET * AnimationUtil.getFadeOut(RenderContext.FADE_ANIM_NAME)); return super.getX(); } @@ -224,9 +218,9 @@ public int getX() { @Override public int getWidth() { if (state == State.FADING_IN) - return super.getWidth() + FADE_OFFSET - (int)(FADE_OFFSET * AnimationUtil.getFadeIn(ANIMATION_FADE_NAME)); + return super.getWidth() + RenderContext.FADE_OFFSET - (int)(RenderContext.FADE_OFFSET * AnimationUtil.getFadeIn(RenderContext.FADE_ANIM_NAME)); if (state == State.FADING_OUT) - return super.getWidth() + (int)(FADE_OFFSET * AnimationUtil.getFadeOut(ANIMATION_FADE_NAME)); + return super.getWidth() + (int)(RenderContext.FADE_OFFSET * AnimationUtil.getFadeOut(RenderContext.FADE_ANIM_NAME)); return super.getWidth(); } @@ -240,8 +234,109 @@ protected boolean isValidClickButton(int pButton) { protected void updateWidgetNarration(@NotNull NarrationElementOutput pNarrationElementOutput) { } - @Override - public void playDownSound(@NotNull SoundManager pHandler) { + private class RenderContext { + static final String FADE_ANIM_NAME = TipWidget.class.getSimpleName() + "_fade"; + static final String PROGRESS_ANIM_NAME = TipWidget.class.getSimpleName() + "_progress"; + static final int FADE_ANIM_LENGTH = 400; + static final int BG_BORDER = 4; + static final int FADE_OFFSET = 16; + static final int LINE_SPACE = 12; + static final int MIN_WIDTH = 160; + static final int MAX_WIDTH = 360; + + List titleLines; + List contentLines; + Size2i imageSize; + Size2i screenSize; + Size2i size; + boolean hasImage; + int totalLineSize; + int BGColor; + int fontColor; + float pYaw; + float pPitch; + + void update(Tip tip) { + BGColor = FHColorHelper.setAlpha(tip.getBackgroundColor(), (isGuiOpened() ? 0.8F : 0.5F)); + fontColor = tip.getFontColor(); + + // 跟随视角晃动 + Minecraft MC = ClientUtils.mc(); + if (MC.player != null) { + if (MC.isPaused()) { + pYaw = (MC.player.getViewYRot(MC.getFrameTime()) - MC.player.yBob) * -0.1F; + pPitch = (MC.player.getViewXRot(MC.getFrameTime()) - MC.player.xBob) * -0.1F; + } else { + pYaw = (MC.player.getViewYRot(MC.getFrameTime()) + - Mth.lerp(MC.getFrameTime(), MC.player.yBobO, MC.player.yBob)) * -0.1F; + pPitch = (MC.player.getViewXRot(MC.getFrameTime()) + - Mth.lerp(MC.getFrameTime(), MC.player.xBobO, MC.player.xBob)) * -0.1F; + } + } else { + pYaw = 0; + pPitch = 0; + } + + // 检查屏幕尺寸是否更新 + Size2i newSize = new Size2i(ClientUtils.screenWidth(), ClientUtils.screenHeight()); + if (this.screenSize != null && this.screenSize.equals(newSize)) return; + this.screenSize = newSize; + int width = (int)Mth.clamp(screenSize.width * 0.3F, MIN_WIDTH, MAX_WIDTH); + + // 文本换行 + var contents = tip.getContents(); + titleLines = ClientUtils.font().split(contents.get(0), width-24); + contentLines = new ArrayList<>(); + if (contents.size() > 1) + for (int i = 1; i < contents.size(); i++) + contentLines.addAll(ClientUtils.font().split(contents.get(i), width-24)); + totalLineSize = titleLines.size() + contentLines.size(); + int height = (totalLineSize * RenderContext.LINE_SPACE); + + // 图片 + hasImage = tip.getImage() != null && tip.getImageSize() != null; + int imgW = 0; + int imgH = 0; + if (hasImage) { + imgW = tip.getImageSize().width; + imgH = tip.getImageSize().height; + // 缩放图片以适应屏幕 + if (Math.abs(imgW - imgH) < 8 && imgW <= 32) { + imgW = imgW * (32 / imgW); + imgH = imgH * (32 / imgH); + } + if (imgW > width) { + float scale = (float)width / imgW; + imgH = (int) (imgH * scale); + imgW = (int) (imgW * scale); + } + if (context.screenSize.height * 0.3F + height + imgH > screenSize.height) { + float availableHeight = screenSize.height - context.screenSize.height * 0.3F - height - 8; + if (availableHeight > 0) { + float scale = availableHeight / imgH; + imgH = (int) (imgH * scale); + imgW = (int) (imgW * scale); + } + } + } + + imageSize = new Size2i(imgW, imgH); + size = new Size2i(width, height + imageSize.height); + } + + void clear() { + titleLines = null; + contentLines = null; + imageSize = null; + screenSize = null; + size = null; + hasImage = false; + totalLineSize = 0; + BGColor = 0; + fontColor = 0; + pYaw = 0; + pPitch = 0; + } } public enum State { diff --git a/src/main/java/com/teammoeg/frostedheart/util/client/FHGuiHelper.java b/src/main/java/com/teammoeg/frostedheart/util/client/FHGuiHelper.java index bf4b96562..cf9090492 100644 --- a/src/main/java/com/teammoeg/frostedheart/util/client/FHGuiHelper.java +++ b/src/main/java/com/teammoeg/frostedheart/util/client/FHGuiHelper.java @@ -36,10 +36,8 @@ import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.Tesselator; -import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.blaze3d.vertex.VertexFormat; -import dev.ftb.mods.ftblibrary.icon.Color4I; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; @@ -356,14 +354,26 @@ public static void bindTexture(ResourceLocation showingImage) { } - public static int drawStrings(GuiGraphics graphics, Font font, FormattedText text, int x, int y, int color, - int maxWidth, int lineSpace, boolean shadow) + /** + * 换行文本并渲染 + * @param maxWidth 单行最大宽度 + * @param lineSpace 行间距 + * @param shadow 文本阴影 + * @return 换行后的行数 + */ + public static int drawWordWarp(GuiGraphics graphics, Font font, FormattedText text, int x, int y, int color, + int maxWidth, int lineSpace, boolean shadow) { List texts = font.split(text, maxWidth); drawStrings(graphics, font, texts, x, y, color, lineSpace, shadow); return texts.size(); } + /** + * 渲染列表中的所有文本 + * @param lineSpace 行间距 + * @param shadow 文本阴影 + */ public static void drawStrings(GuiGraphics graphics, Font font, List texts, int x, int y, int color, int lineSpace, boolean shadow) { diff --git a/src/main/resources/assets/frostedheart/lang/en_us/tips.json b/src/main/resources/assets/frostedheart/lang/en_us/tips.json index 6ef05bf68..181b72be9 100644 --- a/src/main/resources/assets/frostedheart/lang/en_us/tips.json +++ b/src/main/resources/assets/frostedheart/lang/en_us/tips.json @@ -1,12 +1,15 @@ { "tips.frostedheart.empty.title": "Oops, there's nothing here", "tips.frostedheart.error.desc": "Please check if the Modpack is installed properly or report this issue", - "tips.frostedheart.error.other": "[Error] An unknow error has occurred", - "tips.frostedheart.error.load": "[Error] Unable to load file", + "tips.frostedheart.error.other": "[Error] An unknown error occurred", "tips.frostedheart.error.save": "[Error] Unable to save file", - "tips.frostedheart.error.empty": "[Error] No content to display", - "tips.frostedheart.error.invalid": "[Error] Invalid JSON file format", - "tips.frostedheart.error.not_exists": "[Error] File does not exists", + "tips.frostedheart.error.load": "[Error] Unable to load file", + "tips.frostedheart.error.load.no_id": "Tip id is empty/blank", + "tips.frostedheart.error.load.duplicate_id": "Tip id is empty/blank", + "tips.frostedheart.error.load.empty": "No content to display", + "tips.frostedheart.error.load.invalid": "Invalid JSON file format", + "tips.frostedheart.error.load.invalid_image": "No content to display", + "tips.frostedheart.error.load.not_exists": "File does not exists", "gui.frostedheart.tip_editor.title": "Tip Editor", "gui.frostedheart.close": "Close", diff --git a/src/main/resources/assets/frostedheart/lang/zh_cn/tips.json b/src/main/resources/assets/frostedheart/lang/zh_cn/tips.json index 45dbc89da..33dcc22ed 100644 --- a/src/main/resources/assets/frostedheart/lang/zh_cn/tips.json +++ b/src/main/resources/assets/frostedheart/lang/zh_cn/tips.json @@ -1,12 +1,18 @@ { "tips.frostedheart.empty.title": "哎呀,这里什么都没有", "tips.frostedheart.error.desc": "请检查整合包是否正确安装,或向我们报告这个bug", - "tips.frostedheart.error.other": "错误:未知", - "tips.frostedheart.error.load": "错误:无法加载文件", + "tips.frostedheart.error.other": "错误", + "tips.frostedheart.error.display": "错误:无法显示提示", "tips.frostedheart.error.save": "错误:无法保存文件", - "tips.frostedheart.error.empty": "错误:没有内容可供显示", - "tips.frostedheart.error.invalid": "错误:无效的Json文件格式", - "tips.frostedheart.error.not_exists": "错误:文件不存在", + "tips.frostedheart.error.load": "错误:无法加载文件", + "tips.frostedheart.error.no_id": "提示ID为空或不存在", + "tips.frostedheart.error.duplicate_id": "重复的提示ID", + "tips.frostedheart.error.file_not_exists": "文件不存在", + "tips.frostedheart.error.tip_not_exists": "提示不存在", + "tips.frostedheart.error.empty": "没有内容可供显示", + "tips.frostedheart.error.invalid_json": "无效的Json格式", + "tips.frostedheart.error.invalid_image": "无效的纹理资源路径 '%s'", + "tips.frostedheart.error.invalid_digit": "无效的16进制数字 '%s'", "gui.frostedheart.tip_editor.title": "提示编辑器", "gui.frostedheart.close": "关闭", @@ -21,7 +27,7 @@ "tips.frostedheart.music_warning.title": "等一下!", "tips.frostedheart.music_warning.desc1": "关闭音乐可能会错过我们为冬季救援准备的音乐", - "tips.frostedheart.music_warning.desc2": "原版音乐已经被禁用,如果你希望重新开启可以在我们的配置文件中设置: 'config\\frostedheart-client.toml&r'", + "tips.frostedheart.music_warning.desc2": "原版音乐已经被禁用,你可以在配置文件中重新开启:'config\\frostedheart-client.toml&r'", "tips.frostedheart.update.title": "冬季救援有新更新!", "tips.frostedheart.update.desc1": "点击“检查更新”按钮即可前往下载"