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

feat(client-mod): 更新客户端模组

- 添加取消方块更新功能
- 修复保存配置文件问题
- 修复网络通讯打包解包
This commit is contained in:
jhqwqmc
2025-05-12 09:55:17 +08:00
parent 988d498121
commit e9015345e0
14 changed files with 305 additions and 41 deletions

View File

@@ -1962,28 +1962,48 @@ public class PacketConsumers {
data = (byte[]) Reflections.method$DiscardedPayload$dataByteArray.invoke(payload);
}
String decodeData = new String(data, StandardCharsets.UTF_8);
if (!decodeData.endsWith("init")) return;
int firstColon = decodeData.indexOf(':');
if (firstColon == -1) return;
int secondColon = decodeData.indexOf(':', firstColon + 1);
if (secondColon == -1) return;
int clientBlockRegistrySize = Integer.parseInt(decodeData.substring(firstColon + 1, secondColon));
int serverBlockRegistrySize = RegistryUtils.currentBlockRegistrySize();
if (clientBlockRegistrySize != serverBlockRegistrySize) {
Object kickPacket = Reflections.constructor$ClientboundDisconnectPacket.newInstance(
ComponentUtils.adventureToMinecraft(
Component.translatable(
"disconnect.craftengine.block_registry_mismatch",
TranslationArgument.numeric(clientBlockRegistrySize),
TranslationArgument.numeric(serverBlockRegistrySize)
)
)
);
user.nettyChannel().writeAndFlush(kickPacket);
user.nettyChannel().disconnect();
return;
if (decodeData.endsWith("init")) {
int firstColon = decodeData.indexOf(':');
if (firstColon == -1) return;
int secondColon = decodeData.indexOf(':', firstColon + 1);
if (secondColon == -1) return;
String payloadData = decodeData.substring(firstColon + 1, secondColon);
int clientBlockRegistrySize = Integer.parseInt(payloadData);
int serverBlockRegistrySize = RegistryUtils.currentBlockRegistrySize();
if (clientBlockRegistrySize != serverBlockRegistrySize) {
Object kickPacket = Reflections.constructor$ClientboundDisconnectPacket.newInstance(
ComponentUtils.adventureToMinecraft(
Component.translatable(
"disconnect.craftengine.block_registry_mismatch",
TranslationArgument.numeric(clientBlockRegistrySize),
TranslationArgument.numeric(serverBlockRegistrySize)
)
)
);
user.nettyChannel().writeAndFlush(kickPacket);
user.nettyChannel().disconnect();
return;
}
user.setClientModState(true);
} else if (decodeData.endsWith("cancel")) {
if (!VersionHelper.isOrAbove1_20_2()) return;
int firstColon = decodeData.indexOf(':');
if (firstColon == -1) return;
int secondColon = decodeData.indexOf(':', firstColon + 1);
if (secondColon == -1) return;
String payloadData = decodeData.substring(firstColon + 1, secondColon);
boolean cancel = Boolean.parseBoolean(payloadData);
if (cancel) {
user.nettyChannel().writeAndFlush(
Reflections.constructor$ClientboundCustomPayloadPacket.newInstance(
Reflections.constructor$DiscardedPayload.newInstance(
KeyUtils.toResourceLocation(Key.of(NetworkManager.MOD_CHANNEL)),
(":true:cancel").getBytes(StandardCharsets.UTF_8)
)
)
);
}
}
user.setClientModState(true);
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundCustomPayloadPacket", e);

View File

@@ -6659,4 +6659,20 @@ public class Reflections {
"world.entity.projectile.AbstractArrow"
)
);
public static final Class<?> clazz$ClientboundCustomPayloadPacket = requireNonNull(
BukkitReflectionUtils.findReobfOrMojmapClass(
List.of("network.protocol.game.PacketPlayOutCustomPayload", "network.protocol.common.ClientboundCustomPayloadPacket"),
List.of("network.protocol.game.ClientboundCustomPayloadPacket", "network.protocol.common.ClientboundCustomPayloadPacket")
)
);
public static final Constructor<?> constructor$ClientboundCustomPayloadPacket = requireNonNull(
ReflectionUtils.getTheOnlyConstructor(clazz$ClientboundCustomPayloadPacket)
);
// 1.20.2+
public static final Constructor<?> constructor$DiscardedPayload = Optional.ofNullable(clazz$DiscardedPayload)
.map(clazz -> ReflectionUtils.getTheOnlyConstructor(clazz))
.orElse(null);
}

View File

@@ -1,9 +1,12 @@
package net.momirealms.craftengine.fabric.client;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.minecraft.block.Block;
@@ -20,8 +23,10 @@ import net.momirealms.craftengine.fabric.client.network.CraftEnginePayload;
import java.nio.charset.StandardCharsets;
@Environment(EnvType.CLIENT)
public class CraftEngineFabricModClient implements ClientModInitializer {
public static final String MOD_ID = "craftengine";
public static boolean serverInstalled = false;
@Override
public void onInitializeClient() {
@@ -29,6 +34,8 @@ public class CraftEngineFabricModClient implements ClientModInitializer {
PayloadTypeRegistry.configurationC2S().register(CraftEnginePayload.ID, CraftEnginePayload.CODEC);
registerRenderLayer();
ClientConfigurationConnectionEvents.START.register(CraftEngineFabricModClient::initChannel);
ClientConfigurationNetworking.registerGlobalReceiver(CraftEnginePayload.ID, CraftEngineFabricModClient::handleReceiver);
ClientPlayConnectionEvents.DISCONNECT.register((client, handler) -> serverInstalled = false);
}
public static void registerRenderLayer() {
@@ -60,12 +67,32 @@ public class CraftEngineFabricModClient implements ClientModInitializer {
private static void initChannel(ClientConfigurationNetworkHandler handler, MinecraftClient client) {
if (ModConfig.enableNetwork) {
registerChannel(handler);
} else if (ModConfig.enableCancelBlockUpdate) {
registerCancelBlockUpdateChannel(handler);
} else {
ClientConfigurationNetworking.unregisterGlobalReceiver(CraftEnginePayload.ID);
}
}
private static void handleReceiver(CraftEnginePayload payload, ClientConfigurationNetworking.Context context) {
byte[] data = payload.data();
String decodeData = new String(data, StandardCharsets.UTF_8);
if (decodeData.endsWith("cancel")) {
int firstColon = decodeData.indexOf(':');
if (firstColon == -1) return;
int secondColon = decodeData.indexOf(':', firstColon + 1);
if (secondColon == -1) return;
String payloadData = decodeData.substring(firstColon + 1, secondColon);
serverInstalled = Boolean.parseBoolean(payloadData);
}
}
private static void registerChannel(ClientConfigurationNetworkHandler handler) {
ClientConfigurationNetworking.send(new CraftEnginePayload((":" + Block.STATE_IDS.size() + ":init").getBytes(StandardCharsets.UTF_8)));
}
private static void registerCancelBlockUpdateChannel(ClientConfigurationNetworkHandler handler) {
ClientConfigurationNetworking.send(new CraftEnginePayload((":true:cancel").getBytes(StandardCharsets.UTF_8)));
}
}

View File

@@ -3,24 +3,35 @@ package net.momirealms.craftengine.fabric.client.config;
import me.shedaniel.clothconfig2.api.ConfigBuilder;
import me.shedaniel.clothconfig2.api.ConfigCategory;
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
public class ModConfig {
private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("craft-engine-fabric-mod/config.yml");
public static boolean enableNetwork = true;
public static boolean enableCancelBlockUpdate = false;
public static Screen getConfigScreen(Screen parent) {
ConfigBuilder builder = ConfigBuilder.create()
.setParentScreen(parent)
.setSavingRunnable(ModConfig::saveConfig)
.setTitle(Text.translatable("title.craftengine.config"));
ConfigCategory general = builder.getOrCreateCategory(Text.translatable("category.craftengine.general"));
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
general.addEntry(entryBuilder.startBooleanToggle(
Text.translatable("option.craftengine.enable_network")
.formatted(Formatting.WHITE),
Text.translatable("option.craftengine.enable_network")
.formatted(Formatting.WHITE),
enableNetwork)
.setDefaultValue(true)
.setSaveConsumer(newValue -> enableNetwork = newValue)
@@ -29,7 +40,32 @@ public class ModConfig {
.formatted(Formatting.GRAY)
)
.build());
general.addEntry(entryBuilder.startBooleanToggle(
Text.translatable("option.craftengine.enable_cancel_block_update")
.formatted(Formatting.WHITE),
enableCancelBlockUpdate)
.setDefaultValue(false)
.setSaveConsumer(newValue -> enableCancelBlockUpdate = newValue)
.setTooltip(
Text.translatable("tooltip.craftengine.enable_cancel_block_update")
)
.build()
);
return builder.build();
}
private static void saveConfig() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
var data = new java.util.HashMap<String, Object>();
data.put("enable-network", ModConfig.enableNetwork);
data.put("enable-cancel-block-update", ModConfig.enableCancelBlockUpdate);
try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) {
yaml.dump(data, writer);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -9,8 +9,13 @@ public record CraftEnginePayload(byte[] data) implements CustomPayload {
public static final Identifier CRAFTENGINE_PAYLOAD = Identifier.of("craftengine", "payload");
public static final Id<CraftEnginePayload> ID = new Id<>(CraftEnginePayload.CRAFTENGINE_PAYLOAD);
public static final PacketCodec<PacketByteBuf, CraftEnginePayload> CODEC = PacketCodec.of(
(payload, byteBuf) -> byteBuf.writeByteArray(payload.data()),
buf -> new CraftEnginePayload(buf.readByteArray()));
(payload, byteBuf) -> byteBuf.writeBytes(payload.data()),
buf -> {
int i = buf.readableBytes();
byte[] data = new byte[i];
buf.readBytes(data);
return new CraftEnginePayload(data);
});
@Override
public Id<? extends CustomPayload> getId() {

View File

@@ -1,5 +1,7 @@
package net.momirealms.craftengine.fabric;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.block.BlockState;
@@ -9,16 +11,15 @@ import net.momirealms.craftengine.fabric.util.BlockUtils;
import net.momirealms.craftengine.fabric.util.LoggerFilter;
import net.momirealms.craftengine.fabric.util.RegisterBlocks;
import net.momirealms.craftengine.fabric.util.YamlUtils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@Environment(EnvType.CLIENT)
public class CraftEngineFabricMod implements ModInitializer {
private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("craft-engine-fabric-mod/config.yml");
public static final String MOD_ID = "craftengine";
@@ -47,32 +48,25 @@ public class CraftEngineFabricMod implements ModInitializer {
} catch (IOException e) {
e.printStackTrace();
}
Runtime.getRuntime().addShutdownHook(new Thread(this::saveConfig));
}
@SuppressWarnings("unchecked")
private void loadConfig() {
if (!Files.exists(CONFIG_PATH)) {
ModConfig.enableNetwork = true;
ModConfig.enableCancelBlockUpdate = false;
return;
}
try (InputStream inputStream = Files.newInputStream(CONFIG_PATH)) {
Yaml yaml = new Yaml();
var config = yaml.loadAs(inputStream, java.util.Map.class);
if (config == null) {
ModConfig.enableNetwork = true;
ModConfig.enableCancelBlockUpdate = false;
return;
}
ModConfig.enableNetwork = (Boolean) config.getOrDefault("enable-network", true);
} catch (IOException e) {
e.printStackTrace();
}
}
private void saveConfig() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
var data = new java.util.HashMap<String, Object>();
data.put("enable-network", ModConfig.enableNetwork);
try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) {
yaml.dump(data, writer);
ModConfig.enableCancelBlockUpdate = (Boolean) config.getOrDefault("enable-cancel-block-update", false);
} catch (IOException e) {
e.printStackTrace();
}

View File

@@ -0,0 +1,65 @@
package net.momirealms.craftengine.fabric.mixin;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import net.minecraft.world.World;
import net.minecraft.world.WorldAccess;
import net.minecraft.world.WorldView;
import net.minecraft.world.block.WireOrientation;
import net.minecraft.world.tick.ScheduledTickView;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import static net.momirealms.craftengine.fabric.client.CraftEngineFabricModClient.serverInstalled;
import static net.momirealms.craftengine.fabric.client.config.ModConfig.enableCancelBlockUpdate;
@Environment(EnvType.CLIENT)
@Mixin(AbstractBlock.AbstractBlockState.class)
public abstract class AbstractBlockStateMixin {
@Inject(method = "getStateForNeighborUpdate", at = @At("HEAD"), cancellable = true)
private void cancelGetStateForNeighborUpdate(WorldView world, ScheduledTickView tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, Random random, CallbackInfoReturnable<AbstractBlockStateMixin> cir) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
cir.setReturnValue(this);
}
@Inject(method = "neighborUpdate", at = @At("HEAD"), cancellable = true)
private void cancelNeighborUpdate(World world, BlockPos pos, Block sourceBlock, WireOrientation wireOrientation, boolean notify, CallbackInfo ci) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
ci.cancel();
}
@Inject(method = "updateNeighbors*", at = @At("HEAD"), cancellable = true)
private void cancelUpdateNeighbors(WorldAccess world, BlockPos pos, int flags, CallbackInfo ci) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
ci.cancel();
}
@Inject(method = "scheduledTick", at = @At("HEAD"), cancellable = true)
private void cancelScheduledTick(ServerWorld world, BlockPos pos, Random random, CallbackInfo ci) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
ci.cancel();
}
@Inject(method = "canPlaceAt", at = @At("HEAD"), cancellable = true)
private void passCanPlaceAt(WorldView world, BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
cir.setReturnValue(true);
}
@Inject(method = "randomTick", at = @At("HEAD"), cancellable = true)
private void cancelRandomTick(ServerWorld world, BlockPos pos, Random random, CallbackInfo ci) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
ci.cancel();
}
}

View File

@@ -0,0 +1,26 @@
package net.momirealms.craftengine.fabric.mixin;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.block.AbstractRailBlock;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import static net.momirealms.craftengine.fabric.client.CraftEngineFabricModClient.serverInstalled;
import static net.momirealms.craftengine.fabric.client.config.ModConfig.enableCancelBlockUpdate;
@Environment(EnvType.CLIENT)
@Mixin(AbstractRailBlock.class)
public abstract class AbstractRailBlockMixin {
@Inject(method = "updateCurves", at = @At("HEAD"), cancellable = true)
private void cancelUpdateCurves(BlockState state, World world, BlockPos pos, boolean notify, CallbackInfoReturnable<BlockState> cir) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
cir.setReturnValue(state);
}
}

View File

@@ -0,0 +1,27 @@
package net.momirealms.craftengine.fabric.mixin;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.NbtComponent;
import net.minecraft.item.BlockItem;
import net.minecraft.item.ItemPlacementContext;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(BlockItem.class)
public class BlockItemMixin {
@Inject(method = "place*", at = @At("HEAD"), cancellable = true)
private void onPlace(ItemPlacementContext context, CallbackInfoReturnable<ActionResult> cir) {
ItemStack stack = context.getStack();
NbtComponent customData = stack.getComponents().get(DataComponentTypes.CUSTOM_DATA);
if (customData == null) return;
if (customData.contains("craftengine:id")) {
cir.setReturnValue(ActionResult.FAIL);
cir.cancel();
}
}
}

View File

@@ -0,0 +1,26 @@
package net.momirealms.craftengine.fabric.mixin;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.block.BlockState;
import net.minecraft.fluid.FluidState;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import static net.momirealms.craftengine.fabric.client.CraftEngineFabricModClient.serverInstalled;
import static net.momirealms.craftengine.fabric.client.config.ModConfig.enableCancelBlockUpdate;
@Environment(EnvType.CLIENT)
@Mixin(FluidState.class)
public class FluidStateMixin {
@Inject(method = "onScheduledTick", at = @At("HEAD"), cancellable = true)
private void cancelScheduledTick(ServerWorld world, BlockPos pos, BlockState state, CallbackInfo ci) {
if (!enableCancelBlockUpdate || !serverInstalled) return;
ci.cancel();
}
}

View File

@@ -2,6 +2,8 @@
"title.craftengine.config": "CraftEngine Settings",
"category.craftengine.general": "General",
"option.craftengine.enable_network": "Enable custom blocks in server",
"option.craftengine.enable_cancel_block_update": "Enable cancelling block updates in the server",
"tooltip.craftengine.enable_network": "Changes requires re-entering the server to take effect",
"tooltip.craftengine.enable_cancel_block_update": "Only works on servers with CraftEngine installed.",
"disconnect.craftengine.block_registry_mismatch": "Block registry size mismatch. Current: %s. Expected: %s. \n 1. Make sure that the configs are the same as the server's. \n 2. Do not use any mod that might register new block. \n 3. Do not install ViaVersion."
}

View File

@@ -2,6 +2,8 @@
"title.craftengine.config": "CraftEngine设置",
"category.craftengine.general": "通用",
"option.craftengine.enable_network": "启用服务器内自定义方块",
"option.craftengine.enable_cancel_block_update": "启用服务器内取消方块更新",
"tooltip.craftengine.enable_network": "需要重新进入服务器以应用更改",
"tooltip.craftengine.enable_cancel_block_update": "仅在安装了CraftEngine的服务器内生效",
"disconnect.craftengine.block_registry_mismatch": "方块注册表大小不匹配. 当前: %s. 预期: %s \n 1. 确保客户端mod配置与服务端配置一致. \n 2. 不要安装任何会注册新方块的mod. \n 3. 不要使用ViaVersion."
}

View File

@@ -0,0 +1,14 @@
{
"required": true,
"package": "net.momirealms.craftengine.fabric.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"AbstractBlockStateMixin",
"AbstractRailBlockMixin",
"BlockItemMixin",
"FluidStateMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -24,6 +24,10 @@
"modmenu": ["net.momirealms.craftengine.fabric.client.config.ModMenuIntegration"]
},
"mixins": [
"craftengine.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric": "*",