diff --git a/bukkit/compatibility/build.gradle.kts b/bukkit/compatibility/build.gradle.kts index b0cd5fa25..db481a7c5 100644 --- a/bukkit/compatibility/build.gradle.kts +++ b/bukkit/compatibility/build.gradle.kts @@ -26,9 +26,6 @@ dependencies { compileOnly("pers.neige.neigeitems:NeigeItems:1.21.42") // Placeholder compileOnly("me.clip:placeholderapi:${rootProject.properties["placeholder_api_version"]}") - // WorldEdit - compileOnly("com.sk89q.worldedit:worldedit-core:7.2.19") - compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.2.19") // SlimeWorld compileOnly("com.infernalsuite.asp:api:4.0.0-SNAPSHOT") // ModelEngine @@ -44,6 +41,10 @@ dependencies { compileOnly("com.viaversion:viaversion-api:5.3.2") // Skript compileOnly("com.github.SkriptLang:Skript:2.11.0") + // FAWE + compileOnly(platform("com.intellectualsites.bom:bom-newest:1.52")) + compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Core") + compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Bukkit") { isTransitive = false } } java { diff --git a/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/FastAsyncWorldEditDelegate.java b/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/FastAsyncWorldEditDelegate.java new file mode 100644 index 000000000..53987af76 --- /dev/null +++ b/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/FastAsyncWorldEditDelegate.java @@ -0,0 +1,165 @@ +package net.momirealms.craftengine.bukkit.compatibility.worldedit; + +import com.fastasyncworldedit.core.configuration.Settings; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.event.extent.EditSessionEvent; +import com.sk89q.worldedit.extent.AbstractDelegateExtent; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.function.operation.Operation; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.eventbus.Subscribe; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import net.momirealms.craftengine.bukkit.block.BukkitBlockManager; +import net.momirealms.craftengine.bukkit.util.BlockStateUtils; +import net.momirealms.craftengine.core.block.EmptyBlock; +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.world.CEWorld; +import net.momirealms.craftengine.core.world.ChunkPos; +import net.momirealms.craftengine.core.world.chunk.CEChunk; +import org.bukkit.Bukkit; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public class FastAsyncWorldEditDelegate extends AbstractDelegateExtent { + private final Set needSaveChunks; + private final CEWorld ceWorld; + + protected FastAsyncWorldEditDelegate(EditSessionEvent event) { + super(event.getExtent()); + this.needSaveChunks = new HashSet<>(); + var weWorld = event.getWorld(); + var world = Bukkit.getWorld(requireNonNull(weWorld).getName()); + var ceWorld = CraftEngine.instance().worldManager().getWorld(requireNonNull(world).getUID()); + this.ceWorld = requireNonNull(ceWorld); + } + + public static void init() { + Settings.settings().EXTENT.ALLOWED_PLUGINS.add(FastAsyncWorldEditDelegate.class.getCanonicalName()); + WorldEdit.getInstance().getEventBus().register(new Object() { + @Subscribe + @SuppressWarnings("unused") + public void onEditSessionEvent(EditSessionEvent event) { + if (event.getStage() != EditSession.Stage.BEFORE_HISTORY) return; + event.setExtent(new FastAsyncWorldEditDelegate(event)); + } + }); + } + + @Override + public int setBlocks(final Set vset, final Pattern pattern) { + this.processBlocks(vset, pattern); + return super.setBlocks(vset, pattern); + } + + @Override + public int setBlocks(final Region region, final Pattern pattern) { + this.processBlocks(region, pattern); + return super.setBlocks(region, pattern); + } + + + @Override + public > int setBlocks(final Region region, final B block) { + this.processBlocks(region, block); + return super.setBlocks(region, block); + } + + @Override + public int replaceBlocks(Region region, Mask mask, Pattern pattern) { + this.processBlocks(region, pattern); + return super.replaceBlocks(region, mask, pattern); + } + @Override + public > int replaceBlocks(final Region region, final Set filter, final B replacement) { + this.processBlocks(region, replacement); + return super.replaceBlocks(region, filter, replacement); + } + + @Override + public int replaceBlocks(final Region region, final Set filter, final Pattern pattern) { + this.processBlocks(region, pattern); + return super.replaceBlocks(region, filter, pattern); + } + + @Override + public > boolean setBlock(int x, int y, int z, T block) { + try { + BaseBlock oldBlockState = getBlock(x, y, z).toBaseBlock(); + this.processBlock(x, y, z, block.toBaseBlock(), oldBlockState); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Error when recording FastAsyncWorldEdit operation blocks", e); + } + return super.setBlock(x, y, z, block); + } + + @Override + public > boolean setBlock(BlockVector3 position, T block) { + try { + BaseBlock oldBlockState = getBlock(position).toBaseBlock(); + this.processBlock(position.x(), position.y(), position.z(), block.toBaseBlock(), oldBlockState); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Error when recording FastAsyncWorldEdit operation blocks", e); + } + return super.setBlock(position, block); + } + + @Override + protected Operation commitBefore() { + saveAllChunks(); + return super.commitBefore(); + } + + private void processBlocks(Iterable region, Pattern pattern) { + try { + for (BlockVector3 position : region) { + BaseBlock blockState = pattern.applyBlock(position); + BaseBlock oldBlockState = getBlock(position).toBaseBlock(); + int blockX = position.x(); + int blockY = position.y(); + int blockZ = position.z(); + this.processBlock(blockX, blockY, blockZ, blockState, oldBlockState); + } + saveAllChunks(); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Error when recording FastAsyncWorldEdit operation blocks", e); + } + } + + private void processBlock(int blockX, int blockY, int blockZ, BaseBlock blockState, BaseBlock oldBlockState) throws IOException { + int chunkX = blockX >> 4; + int chunkZ = blockZ >> 4; + int stateId = BlockStateUtils.blockDataToId(Bukkit.createBlockData(blockState.getAsString())); + int oldStateId = BlockStateUtils.blockDataToId(Bukkit.createBlockData(oldBlockState.getAsString())); + if (BlockStateUtils.isVanillaBlock(stateId) && BlockStateUtils.isVanillaBlock(oldStateId)) return; + var ceChunk = this.ceWorld.getChunkAtIfLoaded(chunkX, chunkZ); + if (ceChunk == null) { + ceChunk = this.ceWorld.worldDataStorage().readChunkAt(this.ceWorld, new ChunkPos(chunkX, chunkZ)); + } + var immutableBlockState = BukkitBlockManager.instance().getImmutableBlockState(stateId); + if (immutableBlockState == null) { + ceChunk.setBlockState(blockX, blockY, blockZ, EmptyBlock.STATE); + } else { + ceChunk.setBlockState(blockX, blockY, blockZ, immutableBlockState); + } + this.needSaveChunks.add(ceChunk); + } + + private void saveAllChunks() { + try { + for (CEChunk ceChunk : this.needSaveChunks) { + this.ceWorld.worldDataStorage().writeChunkAt(ceChunk.chunkPos(), ceChunk, true); + } + this.needSaveChunks.clear(); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Error when recording FastAsyncWorldEdit operation chunks", e); + } + } +} diff --git a/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/WorldEditBlockRegister.java b/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/WorldEditBlockRegister.java index 2066c2bc1..dd0951843 100644 --- a/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/WorldEditBlockRegister.java +++ b/bukkit/compatibility/src/main/java/net/momirealms/craftengine/bukkit/compatibility/worldedit/WorldEditBlockRegister.java @@ -30,8 +30,12 @@ public class WorldEditBlockRegister { this.isFAWE = isFAWE; CEBlockParser blockParser = new CEBlockParser(WorldEdit.getInstance()); WorldEdit.getInstance().getBlockFactory().register(blockParser); + if (isFAWE) { + FastAsyncWorldEditDelegate.init(); + } } + @SuppressWarnings("deprecation") public void register(Key id) throws ReflectiveOperationException { BlockType blockType = new BlockType(id.toString(), blockState -> blockState); this.field$BlockType$blockMaterial.set(blockType, LazyReference.from(() -> new BukkitBlockRegistry.BukkitBlockMaterial(null, Material.STONE))); @@ -45,6 +49,7 @@ public class WorldEditBlockRegister { } @Override + @SuppressWarnings("deprecation") public Stream getSuggestions(String input) { Set namespacesInUse = manager.namespacesInUse(); diff --git a/bukkit/loader/src/main/resources/translations/en.yml b/bukkit/loader/src/main/resources/translations/en.yml index 7b9afee86..28cf30a1f 100644 --- a/bukkit/loader/src/main/resources/translations/en.yml +++ b/bukkit/loader/src/main/resources/translations/en.yml @@ -80,6 +80,7 @@ warning.config.image.invalid_font_chars: "Issue found in file - warning.config.image.missing_char: "Issue found in file - The image '' is missing the required 'char' argument." warning.config.image.codepoint_conflict: "Issue found in file - The image '' is using a character '()' in font that has been used by another image ''." warning.config.image.invalid_codepoint_grid: "Issue found in file - Image '' has an invalid 'chars' codepoint grid." +warning.config.image.invalid_char: "Issue found in file - Image '' has a char parameter containing combining characters, which may result in image splitting." warning.config.image.file_not_found: "Issue found in file - PNG file '' not found for image ''." warning.config.image.invalid_hex_value: "Issue found in file - The image '' is using a unicode character '' that is not a valid hexadecimal (radix 16) value." warning.config.recipe.duplicate: "Issue found in file - Duplicated recipe ''. Please check if there is the same configuration in other files." diff --git a/bukkit/loader/src/main/resources/translations/zh_cn.yml b/bukkit/loader/src/main/resources/translations/zh_cn.yml index 860a110af..dff59914a 100644 --- a/bukkit/loader/src/main/resources/translations/zh_cn.yml +++ b/bukkit/loader/src/main/resources/translations/zh_cn.yml @@ -80,6 +80,7 @@ warning.config.image.invalid_font_chars: "在文件 发现问题 warning.config.image.missing_char: "在文件 发现问题 - 图片 '' 缺少必需的 'char' 参数" warning.config.image.codepoint_conflict: "在文件 发现问题 - 图片 '' 在字体 中使用的字符 '()' 已被其他图片 '' 占用" warning.config.image.invalid_codepoint_grid: "在文件 发现问题 - 图片 '' 的 'chars' 码位网格无效" +warning.config.image.invalid_char: "在文件 发现问题 - 图片 '' 的 'char' 参数包含组合字符可能导致图片分裂" warning.config.image.file_not_found: "在文件 发现问题 - 图片 '' 的 PNG 文件 '' 未找到" warning.config.image.invalid_hex_value: "在文件 发现问题 - 图片 '' 使用的 Unicode 字符 '' 不是有效的十六进制值" warning.config.recipe.duplicate: "在文件 发现问题 - 重复的配方 '' 请检查其他文件中是否存在相同配置" diff --git a/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java index 771388222..bbb3d4b25 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java @@ -462,7 +462,18 @@ public abstract class AbstractFontManager implements FontManager { if (character.length() == 1) { chars = List.of(character.toCharArray()); } else { - chars = List.of(CharacterUtils.decodeUnicodeToChars(character)); + if (character.startsWith("\\u")) { + chars = List.of(CharacterUtils.decodeUnicodeToChars(character)); + } else { + if (CharacterUtils.containsCombinedCharacter(character)) { + TranslationManager.instance().log("warning.config.image.invalid_char", path.toString(), id.toString()); + } + StringBuilder stringBuilder = new StringBuilder(); + for (char c : character.toCharArray()) { + stringBuilder.append(String.format("\\u%04x", (int) c)); + } + chars = List.of(CharacterUtils.decodeUnicodeToChars(stringBuilder.toString())); + } } } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/CharacterUtils.java b/core/src/main/java/net/momirealms/craftengine/core/util/CharacterUtils.java index 2d8693728..940274bba 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/CharacterUtils.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/CharacterUtils.java @@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.util; import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; +import java.util.regex.Pattern; import java.util.stream.IntStream; public class CharacterUtils { @@ -72,4 +73,27 @@ public class CharacterUtils { } return builder.toString(); } + + public static boolean containsCombinedCharacter(String input) { + if (input == null || input.isEmpty() || input.length() == 1) return false; + for (int i = 0; i < input.length();) { + int codePoint = input.codePointAt(i); + i += Character.charCount(codePoint); + int type = Character.getType(codePoint); + if (type == Character.NON_SPACING_MARK || + type == Character.ENCLOSING_MARK || + type == Character.COMBINING_SPACING_MARK || + type == Character.FORMAT || + type == Character.CONTROL || + type == Character.SURROGATE || + type == Character.PRIVATE_USE || + Pattern.compile("[\\p{Mn}\\p{Me}\\p{Mc}\\p{Cf}]").matcher(new String(Character.toChars(codePoint))).find() + ) return true; + if (i < input.length()) { + int nextCodePoint = input.codePointAt(i); + if (Character.isSurrogatePair(Character.toChars(codePoint)[0], Character.toChars(nextCodePoint)[0])) return true; + } + } + return false; + } } diff --git a/gradle.properties b/gradle.properties index a91348e1c..30c9a5e26 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.jvmargs=-Xmx1G # Rule: [major update].[feature update].[bug fix] project_version=0.0.53-beta.1 config_version=31 -lang_version=9 +lang_version=10 project_group=net.momirealms latest_supported_version=1.21.5