9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-25 09:59:20 +00:00

优化冒险模式挖掘

This commit is contained in:
XiaoMoMi
2025-04-14 21:14:12 +08:00
parent b7e7c9990f
commit e2e8ef0f6a
11 changed files with 233 additions and 182 deletions

View File

@@ -130,30 +130,53 @@ item:
non-italic-tag: false
block:
# Enables the sound system, which prevents the client from hearing some non-custom block sounds and improves the client experience.
sound-system:
enable: true
# In Adventure Mode, players need the correct tool to break custom blocks.
# Vanilla clients DO NOT recognize custom block IDs (e.g., craftengine:note_block_0).
#
# - When ENABLED:
# - Players can break custom blocks if their tools can mine their VANILLA EQUIVALENTS.
# Example: A tool for "note_block" can break "craftengine:note_block_0".
#
# - When DISABLED:
# ⚠️ WARNING:
# - Server MUST list ACTUAL CUSTOM BLOCK IDs in item's `can_break` component.
# - Sending custom IDs (e.g., craftengine:note_block_0) to vanilla clients WILL CRASH THEM!
# ✅ Solution:
# - Use `client-bound-item-data` to safely sync custom block data to clients.
# Documentation: https://mo-mi.gitbook.io/xiaomomi-plugins/craftengine/plugin-wiki/craftengine/add-new-contents/items/item-data/client-bound-item-data
simplify-adventure-break-check: true
furniture:
# Should the plugin remove invalid furniture on chunk load
# Automatically remove outdated furniture entities when a chunk is loaded.
remove-invalid-furniture-on-chunk-load:
# Enable/disable the cleanup system
enable: false
# If you want to remove all invalid furniture, please set this list to empty, otherwise only furniture in the list will be removed.
# - When EMPTY: Remove ALL invalid furniture entities
# - When POPULATED: Only remove specified furniture types
# Example for targeted removal:
# list: [ "xxx:invalid_furniture", "yyy:broken_sofa" ]
list:
- "xxx:invalid_furniture"
# Whether to hide the entity containing metadata
# Hide technical entities used for storing furniture metadata.
# NOTE:
# - These are INVISIBLE entities used internally for tracking furniture states
# - Recommended to keep enabled for better performance
hide-base-entity: true
image:
# Prevent players from using images set in minecraft:default font
# Players with `craftengine.filter.bypass.xxx` would ignore the limitation
# Block image tags using minecraft:default font in these interfaces
# Permission bypass: craftengine.filter.bypass.xxx (replace xxx with context: anvil/book/chat/etc)
illegal-characters-filter:
anvil: true
book: true
chat: true
command: true
sign: true
# By intercepting packets, you are allowed to use <image:...> <shift:...> in other plugins
# Turning off some unused options would help reduce CPU usage on async threads
# Allow <image:...> and <shift:...> tags in third-party plugins via packet manipulation
# ⚠️ Disable unused handlers to reduce async thread workload
intercept-packets:
system-chat: true
tab-list: true # Tab list header and footer
@@ -168,17 +191,69 @@ image:
entity-name: false
armor-stand: true # Legacy Holograms
text-display: true # Modern Holograms
# Defines Unicode characters used for <shift:xxx> positioning
# - Must match the font defined in resource packs
# - Do NOT modify unless you understand text rendering mechanics
offset-characters:
font: minecraft:offset_chars
-1: '\uf800'
-2: '\uf801'
-3: '\uf802'
-4: '\uf803'
-5: '\uf804'
-6: '\uf805'
-7: '\uf806'
-8: '\uf807'
-9: '\uf808'
-10: '\uf809'
-11: '\uf80a'
-12: '\uf80b'
-13: '\uf80c'
-14: '\uf80d'
-15: '\uf80e'
-16: '\uf80f'
-24: '\uf810'
-32: '\uf811'
-48: '\uf812'
-64: '\uf813'
-128: '\uf814'
-256: '\uf815'
1: '\uf830'
2: '\uf831'
3: '\uf832'
4: '\uf833'
5: '\uf834'
6: '\uf835'
7: '\uf836'
8: '\uf837'
9: '\uf838'
10: '\uf839'
11: '\uf83a'
12: '\uf83b'
13: '\uf83c'
14: '\uf83d'
15: '\uf83e'
16: '\uf83f'
24: '\uf840'
32: '\uf841'
48: '\uf842'
64: '\uf843'
128: '\uf844'
256: '\uf845'
emoji: {}
recipe:
# Enable the plugin's recipe system
# Master switch for custom recipes
# NOTE: When enabled, plugin recipes will OVERRIDE vanilla recipes
enable: true
# Disable vanilla recipes
# Manage Minecraft's default recipe behavior
disable-vanilla-recipes:
# Disable all vanilla recipes
# ⚠️ WARNING: When true, DISABLES ALL VANILLA RECIPES
# - Conflicts with 'list' option (list will be ignored)
all: false
# Disable the recipes in list
# Selective recipe disabling (safer alternative to 'all: true')
# Example: ["minecraft:wooden_sword", "minecraft:stone_hoe"]
list: []
gui:
@@ -240,12 +315,13 @@ gui:
performance:
# Maximum chain update depth when fixing client visuals
max-block-chain-update-limit: 64
# Maximum number of emojis to parse per operation
# Prevent lag or oversized packet when processing emoji-heavy content
max-emojis-per-parse: 16
light-system:
# Required for custom light-emitting blocks
enable: true
# Turning this option on will reduce lighting system issues to some extent, but will increase server bandwidth consumption
# Turning this option on will reduce lighting system issues to some extent, but will increase server bandwidth consumption.
force-update-light: false
chunk-system:
@@ -255,59 +331,28 @@ chunk-system:
# 4 = LZ4 | Blazing-Fast Blazing-Fast Low Low |
# 5 = ZSTD | Medium-Fast Fast High Medium |
compression-method: 4
# Disabling this option prevents the plugin from converting custom blocks to vanilla states when chunks are unloaded.
# While this can improve performance, custom blocks will turn into air if the plugin is uninstalled.
# Auto-convert custom blocks -> vanilla blocks when unloading chunks
#
# - When ENABLED (true):
# - Prevents custom blocks becoming AIR if plugin is uninstalled
# - Ensures world portability for vanilla environments
#
# - When DISABLED (false):
# ⚠️ IRREVERSIBLE DATA LOSS WARNING:
# - Custom blocks permanently turn to AIR without plugin
# - Recommended for temporary/test worlds only
restore-vanilla-blocks-on-chunk-unload: true
# Convert vanilla blocks -> custom blocks when loading chunks
#
# - Performance Mode (false):
# ⚠️ REQUIRED CONDITIONS:
# 1. Must disable restore-vanilla-blocks-on-chunk-unload
# 2. Accept risk of custom block data loss on plugin removal
#
# - Compatibility Mode (true):
# - Full state recovery with minor performance cost
restore-custom-blocks-on-chunk-load: true
# When you edit a map locally using CraftEngine fabric mod, the custom block data is not immediately synchronized with the
# server's CraftEngine internal data. Enabling this option will synchronize the data when the chunk is loaded.
# (This option only slightly impacts performance, which has been fully optimized, so you don't need to worry too much.)
sync-custom-blocks-on-chunk-load: false
# If you disable this, it's a must to disable the above option.
restore-custom-blocks-on-chunk-load: true
offset-characters:
font: minecraft:offset_chars
-1: '\uf800'
-2: '\uf801'
-3: '\uf802'
-4: '\uf803'
-5: '\uf804'
-6: '\uf805'
-7: '\uf806'
-8: '\uf807'
-9: '\uf808'
-10: '\uf809'
-11: '\uf80a'
-12: '\uf80b'
-13: '\uf80c'
-14: '\uf80d'
-15: '\uf80e'
-16: '\uf80f'
-24: '\uf810'
-32: '\uf811'
-48: '\uf812'
-64: '\uf813'
-128: '\uf814'
-256: '\uf815'
1: '\uf830'
2: '\uf831'
3: '\uf832'
4: '\uf833'
5: '\uf834'
6: '\uf835'
7: '\uf836'
8: '\uf837'
9: '\uf838'
10: '\uf839'
11: '\uf83a'
12: '\uf83b'
13: '\uf83c'
14: '\uf83d'
15: '\uf83e'
16: '\uf83f'
24: '\uf840'
32: '\uf841'
48: '\uf842'
64: '\uf843'
128: '\uf844'
256: '\uf845'
sync-custom-blocks-on-chunk-load: false

View File

@@ -105,6 +105,27 @@ public class BlockEventListener implements Listener {
ImmutableBlockState state = manager.getImmutableBlockStateUnsafe(stateId);
if (!state.isEmpty()) {
Location location = block.getLocation();
BukkitServerPlayer serverPlayer = this.plugin.adapt(player);
// double check to prevent dupe
if (serverPlayer.isAdventureMode()) {
Object itemStack = FastNMS.INSTANCE.method$CraftItemStack$asNMSCopy(player.getInventory().getItemInMainHand());
Object blockPos = LocationUtils.toBlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ());
try {
Object blockInWorld = Reflections.constructor$BlockInWorld.newInstance(FastNMS.INSTANCE.field$CraftWorld$ServerLevel(location.getWorld()), blockPos, false);
if (VersionHelper.isVersionNewerThan1_20_5()) {
if (Reflections.method$ItemStack$canBreakBlockInAdventureMode != null && !(boolean) Reflections.method$ItemStack$canBreakBlockInAdventureMode.invoke(itemStack, blockInWorld)) {
return;
}
} else {
if (Reflections.method$ItemStack$canDestroy != null && !(boolean) Reflections.method$ItemStack$canDestroy.invoke(itemStack, Reflections.instance$BuiltInRegistries$BLOCK, blockInWorld)) {
return;
}
}
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to double check adventure mode", e);
return;
}
}
// trigger event
CustomBlockBreakEvent customBreakEvent = new CustomBlockBreakEvent(event.getPlayer(), location, block, state);
@@ -131,7 +152,6 @@ public class BlockEventListener implements Listener {
return;
}
BukkitServerPlayer serverPlayer = this.plugin.adapt(player);
Item<ItemStack> itemInHand = serverPlayer.getItemInHand(InteractionHand.MAIN_HAND);
Key itemId = Optional.ofNullable(itemInHand).map(Item::id).orElse(ItemKeys.AIR);
// do not drop if it's not the correct tool

View File

@@ -76,22 +76,25 @@ public class BlockItemBehavior extends ItemBehavior {
return InteractionResult.FAIL;
}
Player player = placeContext.getPlayer();
int gameTicks = player.gameTicks();
if (!player.updateLastSuccessfulInteractionTick(gameTicks)) {
return InteractionResult.FAIL;
}
BlockPos pos = placeContext.getClickedPos();
BlockPos againstPos = placeContext.getAgainstPos();
World world = (World) placeContext.getLevel().platformWorld();
Location placeLocation = new Location(world, pos.x(), pos.y(), pos.z());
// todo adventure check
if (player.isAdventureMode()) {
}
int gameTicks = player.gameTicks();
if (!player.updateLastSuccessfulInteractionTick(gameTicks)) {
return InteractionResult.FAIL;
}
Block bukkitBlock = world.getBlockAt(placeLocation);
Block againstBlock = world.getBlockAt(againstPos.x(), againstPos.y(), againstPos.z());
org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) player.platformPlayer();
// todo adventure check
// trigger event
CustomBlockAttemptPlaceEvent attemptPlaceEvent = new CustomBlockAttemptPlaceEvent(bukkitPlayer, placeLocation.clone(), blockStateToPlace,
DirectionUtils.toBlockFace(context.getClickedFace()), bukkitBlock, context.getHand());

View File

@@ -1127,23 +1127,17 @@ public class PacketConsumers {
}
return;
}
if (player.isAdventureMode()) {
if (player.isAdventureMode() && !Config.simplyAdventureCheck()) {
Object itemStack = FastNMS.INSTANCE.method$CraftItemStack$asNMSCopy(player.platformPlayer().getInventory().getItemInMainHand());
Object blockPos = LocationUtils.toBlockPos(pos);
Object blockInWorld = Reflections.constructor$BlockInWorld.newInstance(serverLevel, blockPos, false);
if (VersionHelper.isVersionNewerThan1_20_5()) {
if (Reflections.method$ItemStack$canBreakBlockInAdventureMode != null
&& !(boolean) Reflections.method$ItemStack$canBreakBlockInAdventureMode.invoke(
itemStack, blockInWorld
)) {
if (Reflections.method$ItemStack$canBreakBlockInAdventureMode != null && !(boolean) Reflections.method$ItemStack$canBreakBlockInAdventureMode.invoke(itemStack, blockInWorld)) {
player.preventMiningBlock();
return;
}
} else {
if (Reflections.method$ItemStack$canDestroy != null
&& !(boolean) Reflections.method$ItemStack$canDestroy.invoke(
itemStack, Reflections.instance$BuiltInRegistries$BLOCK, blockInWorld
)) {
if (Reflections.method$ItemStack$canDestroy != null && !(boolean) Reflections.method$ItemStack$canDestroy.invoke(itemStack, Reflections.instance$BuiltInRegistries$BLOCK, blockInWorld)) {
player.preventMiningBlock();
return;
}

View File

@@ -16,6 +16,7 @@ import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemKeys;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
@@ -215,31 +216,6 @@ public class BukkitServerPlayer extends Player {
platformPlayer().closeInventory();
}
// TODO DO NOT USE BUKKIT API
@Override
public BlockHitResult rayTrace(double distance, FluidCollisionRule collisionRule) {
RayTraceResult result = platformPlayer().rayTraceBlocks(distance, FluidUtils.toCollisionRule(collisionRule));
if (result == null) {
Location eyeLocation = platformPlayer().getEyeLocation();
Location targetLocation = eyeLocation.clone();
targetLocation.add(eyeLocation.getDirection().multiply(distance));
return BlockHitResult.miss(new Vec3d(eyeLocation.getX(), eyeLocation.getY(), eyeLocation.getZ()),
Direction.getApproximateNearest(eyeLocation.getX() - targetLocation.getX(), eyeLocation.getY() - targetLocation.getY(), eyeLocation.getZ() - targetLocation.getZ()),
new BlockPos(targetLocation.getBlockX(), targetLocation.getBlockY(), targetLocation.getBlockZ())
);
} else {
Vector hitPos = result.getHitPosition();
Block hitBlock = result.getHitBlock();
Location hitBlockLocation = hitBlock.getLocation();
return new BlockHitResult(
new Vec3d(hitPos.getX(), hitPos.getY(), hitPos.getZ()),
DirectionUtils.toDirection(result.getHitBlockFace()),
new BlockPos(hitBlockLocation.getBlockX(), hitBlockLocation.getBlockY(), hitBlockLocation.getBlockZ()),
false
);
}
}
@Override
public void sendPacket(Object packet, boolean immediately) {
this.plugin.networkManager().sendPacket(this, packet, immediately);
@@ -463,8 +439,13 @@ public class BukkitServerPlayer extends Player {
}
if (this.miningProgress >= 1f) {
//Reflections.method$ServerLevel$levelEvent.invoke(Reflections.field$CraftWorld$ServerLevel.get(player.getWorld()), null, 2001, blockPos, BlockStateUtils.blockStateToId(this.destroyedState));
Reflections.method$ServerPlayerGameMode$destroyBlock.invoke(gameMode, blockPos);
if (isAdventureMode() && Config.simplyAdventureCheck()) {
player.setGameMode(GameMode.SURVIVAL);
Reflections.method$ServerPlayerGameMode$destroyBlock.invoke(gameMode, blockPos);
player.setGameMode(GameMode.ADVENTURE);
} else {
Reflections.method$ServerPlayerGameMode$destroyBlock.invoke(gameMode, blockPos);
}
Object levelEventPacket = Reflections.constructor$ClientboundLevelEventPacket.newInstance(2001, blockPos, id, false);
sendPacket(levelEventPacket, false);
this.stopMiningBlock();

View File

@@ -75,7 +75,5 @@ public abstract class Player extends Entity implements NetWorkUser {
public abstract void closeInventory();
public abstract BlockHitResult rayTrace(double distance, FluidCollisionRule collisionRule);
public abstract void clearView();
}

View File

@@ -51,7 +51,7 @@ public abstract class AbstractFontManager implements FontManager {
@Override
public void load() {
this.offsetFont = Optional.ofNullable(plugin.config().settings().getSection("offset-characters"))
this.offsetFont = Optional.ofNullable(plugin.config().settings().getSection("image.offset-characters"))
.map(OffsetFont::new)
.orElse(null);
}

View File

@@ -32,6 +32,7 @@ import net.momirealms.craftengine.core.sound.SoundManager;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.world.WorldManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import java.util.ArrayList;
import java.util.List;
@@ -90,8 +91,8 @@ public abstract class CraftEngine implements Plugin {
}
public void onPluginLoad() {
((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addFilter(new LogFilter());
((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addFilter(new DisconnectLogFilter());
((Logger) LogManager.getRootLogger()).addFilter(new LogFilter());
((Logger) LogManager.getRootLogger()).addFilter(new DisconnectLogFilter());
this.dependencyManager = new DependencyManagerImpl(this);
ArrayList<Dependency> dependenciesToLoad = new ArrayList<>();
dependenciesToLoad.addAll(commonDependencies());
@@ -115,72 +116,77 @@ public abstract class CraftEngine implements Plugin {
public CompletableFuture<ReloadResult> reloadPlugin(Executor asyncExecutor, Executor syncExecutor, boolean reloadRecipe) {
CompletableFuture<ReloadResult> future = new CompletableFuture<>();
asyncExecutor.execute(() -> {
if (this.isReloading) {
future.complete(ReloadResult.failure());
return;
}
this.isReloading = true;
long time1 = System.currentTimeMillis();
// firstly reload main config
this.config.load();
// reset debugger
this.debugger = Config.debug() ? (s) -> logger.info("[Debug] " + s.get()) : (s) -> {};
// now we reload the translations
this.translationManager.reload();
// clear the outdated cache by reloading the managers
this.templateManager.reload();
this.furnitureManager.reload();
this.fontManager.reload();
this.itemManager.reload();
this.soundManager.reload();
this.itemBrowserManager.reload();
this.blockManager.reload();
this.worldManager.reload();
this.vanillaLootManager.reload();
this.guiManager.reload();
this.packManager.reload();
if (reloadRecipe) {
this.recipeManager.reload();
}
long asyncTime = -1;
try {
// now we load resources
this.packManager.loadResources(reloadRecipe);
} catch (Exception e) {
this.logger().warn("Failed to load resources folder", e);
}
// init suggestions and packet mapper
this.blockManager.delayedLoad();
// handle some special client lang for instance block_name
this.translationManager.delayedLoad();
// init suggestions
this.furnitureManager.delayedLoad();
// sort the categories
this.itemBrowserManager.delayedLoad();
// collect illegal characters from minecraft:default font
this.fontManager.delayedLoad();
if (reloadRecipe) {
// convert data pack recipes
this.recipeManager.delayedLoad();
}
long time2 = System.currentTimeMillis();
long asyncTime = time2 - time1;
syncExecutor.execute(() -> {
try {
long time3 = System.currentTimeMillis();
// register songs
this.soundManager.runDelayedSyncTasks();
// register recipes
if (reloadRecipe) {
this.recipeManager.runDelayedSyncTasks();
}
long time4 = System.currentTimeMillis();
long syncTime = time4 - time3;
this.reloadEventDispatcher.accept(this);
future.complete(ReloadResult.success(asyncTime, syncTime));
} finally {
this.isReloading = false;
if (this.isReloading) {
future.complete(ReloadResult.failure());
return;
}
});
this.isReloading = true;
long time1 = System.currentTimeMillis();
// firstly reload main config
this.config.load();
// reset debugger
this.debugger = Config.debug() ? (s) -> logger.info("[Debug] " + s.get()) : (s) -> {};
// now we reload the translations
this.translationManager.reload();
// clear the outdated cache by reloading the managers
this.templateManager.reload();
this.furnitureManager.reload();
this.fontManager.reload();
this.itemManager.reload();
this.soundManager.reload();
this.itemBrowserManager.reload();
this.blockManager.reload();
this.worldManager.reload();
this.vanillaLootManager.reload();
this.guiManager.reload();
this.packManager.reload();
if (reloadRecipe) {
this.recipeManager.reload();
}
try {
// now we load resources
this.packManager.loadResources(reloadRecipe);
} catch (Exception e) {
this.logger().warn("Failed to load resources folder", e);
}
// init suggestions and packet mapper
this.blockManager.delayedLoad();
// handle some special client lang for instance block_name
this.translationManager.delayedLoad();
// init suggestions
this.furnitureManager.delayedLoad();
// sort the categories
this.itemBrowserManager.delayedLoad();
// collect illegal characters from minecraft:default font
this.fontManager.delayedLoad();
if (reloadRecipe) {
// convert data pack recipes
this.recipeManager.delayedLoad();
}
long time2 = System.currentTimeMillis();
asyncTime = time2 - time1;
} finally {
long finalAsyncTime = asyncTime;
syncExecutor.execute(() -> {
try {
long time3 = System.currentTimeMillis();
// register songs
this.soundManager.runDelayedSyncTasks();
// register recipes
if (reloadRecipe) {
this.recipeManager.runDelayedSyncTasks();
}
long time4 = System.currentTimeMillis();
long syncTime = time4 - time3;
this.reloadEventDispatcher.accept(this);
future.complete(ReloadResult.success(finalAsyncTime, syncTime));
} finally {
this.isReloading = false;
}
});
}
});
return future;
}

View File

@@ -111,6 +111,7 @@ public class Config {
protected boolean furniture$hide_base_entity;
protected boolean block$sound_system$enable;
protected boolean block$simply_adventure_break_check;
protected boolean recipe$enable;
protected boolean recipe$disable_vanilla_recipes$all;
@@ -281,6 +282,7 @@ public class Config {
// block
block$sound_system$enable = config.getBoolean("block.sound-system.enable", true);
block$simply_adventure_break_check = config.getBoolean("block.simplify-adventure-break-check", true);
// recipe
recipe$enable = config.getBoolean("recipe.enable", true);
@@ -390,6 +392,10 @@ public class Config {
return instance.block$sound_system$enable;
}
public static boolean simplyAdventureCheck() {
return instance.block$simply_adventure_break_check;
}
public static boolean enableRecipeSystem() {
return instance.recipe$enable;
}

View File

@@ -2,8 +2,8 @@ org.gradle.jvmargs=-Xmx1G
# Project settings
# Rule: [major update].[feature update].[bug fix]
project_version=0.0.47.4
config_version=25
project_version=0.0.47.5
config_version=26
lang_version=4
project_group=net.momirealms
latest_supported_version=1.21.5

View File

@@ -14,14 +14,12 @@ import net.momirealms.craftengine.mod.util.NoteBlockUtils;
import net.momirealms.craftengine.mod.util.Reflections;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public class CustomBlocks {