mirror of
https://github.com/EngineHub/WorldEdit.git
synced 2025-02-17 13:20:23 +08:00
Merge branch 'version/7.2.x'
This commit is contained in:
commit
10cd6820c9
@ -53,7 +53,7 @@
|
||||
implementation("org.jfrog.buildinfo:build-info-extractor-gradle:4.32.0")
|
||||
implementation("org.spongepowered:spongegradle-plugin-development:2.1.1")
|
||||
implementation("org.spongepowered:vanillagradle:0.2.1-20230603.203956-54")
|
||||
implementation("net.minecraftforge.gradle:ForgeGradle:6.0.11")
|
||||
implementation("net.minecraftforge.gradle:ForgeGradle:6.0.13")
|
||||
implementation("net.fabricmc:fabric-loom:$loomVersion")
|
||||
implementation("net.fabricmc:sponge-mixin:$mixinVersion")
|
||||
implementation("org.enginehub.gradle:gradle-codecov-plugin:0.2.0")
|
||||
|
@ -4,5 +4,5 @@ version=7.3.0-SNAPSHOT
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.parallel=true
|
||||
|
||||
loom.version=1.2.7
|
||||
loom.version=1.3.9
|
||||
mixin.version=0.12.5+mixin.0.8.5
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
include("worldedit-libs")
|
||||
|
||||
listOf("1.17.1", "1.18.2", "1.19.4", "1.20").forEach {
|
||||
listOf("1.17.1", "1.18.2", "1.19.4", "1.20", "1.20.2").forEach {
|
||||
include("worldedit-bukkit:adapters:adapter-$it")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension
|
||||
|
||||
applyPaperweightAdapterConfiguration()
|
||||
|
||||
dependencies {
|
||||
// https://repo.papermc.io/service/rest/repository/browse/maven-public/io/papermc/paper/dev-bundle/
|
||||
the<PaperweightUserDependenciesExtension>().paperDevBundle("1.20.2-R0.1-20230924.232656-10")
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.bukkit.adapter.impl.v1_20_R2;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ClientInformation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stat;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.player.ChatVisiblity;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
class PaperweightFakePlayer extends ServerPlayer {
|
||||
private static final GameProfile FAKE_WORLDEDIT_PROFILE = new GameProfile(UUID.nameUUIDFromBytes("worldedit".getBytes()), "[WorldEdit]");
|
||||
private static final Vec3 ORIGIN = new Vec3(0.0D, 0.0D, 0.0D);
|
||||
private static final ClientInformation FAKE_CLIENT_INFO = new ClientInformation(
|
||||
"en_US", 16, ChatVisiblity.FULL, true, 0, HumanoidArm.LEFT, false, false
|
||||
);
|
||||
|
||||
PaperweightFakePlayer(ServerLevel world) {
|
||||
super(world.getServer(), world, FAKE_WORLDEDIT_PROFILE, FAKE_CLIENT_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 position() {
|
||||
return ORIGIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void die(DamageSource damagesource) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity changeDimension(ServerLevel worldserver, TeleportCause cause) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt openMenu(MenuProvider factory) {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateOptions(ClientInformation clientOptions) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayClientMessage(Component message, boolean actionBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void awardStat(Stat<?> stat, int amount) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void awardStat(Stat<?> stat) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvulnerableTo(DamageSource damageSource) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openTextEdit(SignBlockEntity sign, boolean front) {
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.bukkit.adapter.impl.v1_20_R2;
|
||||
|
||||
import com.sk89q.worldedit.EditSession;
|
||||
import com.sk89q.worldedit.MaxChangedBlocksException;
|
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter;
|
||||
import com.sk89q.worldedit.entity.BaseEntity;
|
||||
import com.sk89q.worldedit.math.BlockVector3;
|
||||
import com.sk89q.worldedit.util.Location;
|
||||
import com.sk89q.worldedit.util.concurrency.LazyReference;
|
||||
import com.sk89q.worldedit.world.block.BlockTypes;
|
||||
import com.sk89q.worldedit.world.entity.EntityTypes;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.enginehub.linbus.tree.LinCompoundTag;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
public class PaperweightServerLevelDelegateProxy implements InvocationHandler {
|
||||
|
||||
private final EditSession editSession;
|
||||
private final ServerLevel serverLevel;
|
||||
private final PaperweightAdapter adapter;
|
||||
|
||||
private PaperweightServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) {
|
||||
this.editSession = editSession;
|
||||
this.serverLevel = serverLevel;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) {
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
serverLevel.getClass().getClassLoader(),
|
||||
serverLevel.getClass().getInterfaces(),
|
||||
new PaperweightServerLevelDelegateProxy(editSession, serverLevel, adapter)
|
||||
);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BlockEntity getBlockEntity(BlockPos blockPos) {
|
||||
BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos);
|
||||
if (tileEntity == null) {
|
||||
return null;
|
||||
}
|
||||
BlockEntity newEntity = tileEntity.getType().create(blockPos, getBlockState(blockPos));
|
||||
newEntity.load((CompoundTag) adapter.fromNative(this.editSession.getFullBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ())).getNbtReference().getValue()));
|
||||
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
private BlockState getBlockState(BlockPos blockPos) {
|
||||
return adapter.adapt(this.editSession.getBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ())));
|
||||
}
|
||||
|
||||
private boolean setBlock(BlockPos blockPos, BlockState blockState) {
|
||||
try {
|
||||
return editSession.setBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()), adapter.adapt(blockState));
|
||||
} catch (MaxChangedBlocksException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean removeBlock(BlockPos blockPos, boolean bl) {
|
||||
try {
|
||||
return editSession.setBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()), BlockTypes.AIR.getDefaultState());
|
||||
} catch (MaxChangedBlocksException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean addEntity(Entity entity) {
|
||||
Vec3 pos = entity.getPosition(0.0f);
|
||||
Location location = new Location(BukkitAdapter.adapt(serverLevel.getWorld()), pos.x(), pos.y(), pos.z());
|
||||
|
||||
ResourceLocation id = serverLevel.registryAccess().registryOrThrow(Registries.ENTITY_TYPE).getKey(entity.getType());
|
||||
CompoundTag tag = new CompoundTag();
|
||||
entity.saveWithoutId(tag);
|
||||
BaseEntity baseEntity = new BaseEntity(EntityTypes.get(id.toString()), LazyReference.from(() -> (LinCompoundTag) adapter.toNative(tag)));
|
||||
|
||||
return editSession.createEntity(location, baseEntity) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
switch (method.getName()) {
|
||||
case "a_", "getBlockState", "addFreshEntityWithPassengers" -> {
|
||||
if (args.length == 1 && args[0] instanceof BlockPos blockPos) {
|
||||
// getBlockState
|
||||
return getBlockState(blockPos);
|
||||
} else if (args.length >= 1 && args[0] instanceof Entity entity) {
|
||||
// addFreshEntityWithPassengers
|
||||
return addEntity(entity);
|
||||
}
|
||||
}
|
||||
case "c_", "getBlockEntity" -> {
|
||||
if (args.length == 1 && args[0] instanceof BlockPos blockPos) {
|
||||
// getBlockEntity
|
||||
return getBlockEntity(blockPos);
|
||||
}
|
||||
}
|
||||
case "a", "setBlock", "removeBlock", "destroyBlock" -> {
|
||||
if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof BlockState blockState) {
|
||||
// setBlock
|
||||
return setBlock(blockPos, blockState);
|
||||
} else if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof Boolean bl) {
|
||||
// removeBlock (and also matches destroyBlock)
|
||||
return removeBlock(blockPos, bl);
|
||||
}
|
||||
}
|
||||
case "j", "addEntity" -> {
|
||||
if (args.length >= 1 && args[0] instanceof Entity entity) {
|
||||
return addEntity(entity);
|
||||
}
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
|
||||
return method.invoke(this.serverLevel, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.bukkit.adapter.impl.v1_20_R2;
|
||||
|
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter;
|
||||
import com.sk89q.worldedit.internal.block.BlockStateIdAccess;
|
||||
import com.sk89q.worldedit.internal.wna.WorldNativeAccess;
|
||||
import com.sk89q.worldedit.util.SideEffect;
|
||||
import com.sk89q.worldedit.util.SideEffectSet;
|
||||
import com.sk89q.worldedit.world.block.BlockState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.server.level.FullChunkStatus;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.CraftWorld;
|
||||
import org.bukkit.craftbukkit.v1_20_R2.block.data.CraftBlockData;
|
||||
import org.bukkit.event.block.BlockPhysicsEvent;
|
||||
import org.enginehub.linbus.tree.LinCompoundTag;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class PaperweightWorldNativeAccess implements WorldNativeAccess<LevelChunk, net.minecraft.world.level.block.state.BlockState, BlockPos> {
|
||||
private static final int UPDATE = 1;
|
||||
private static final int NOTIFY = 2;
|
||||
|
||||
private final PaperweightAdapter adapter;
|
||||
private final WeakReference<ServerLevel> world;
|
||||
private SideEffectSet sideEffectSet;
|
||||
|
||||
public PaperweightWorldNativeAccess(PaperweightAdapter adapter, WeakReference<ServerLevel> world) {
|
||||
this.adapter = adapter;
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
private ServerLevel getWorld() {
|
||||
return Objects.requireNonNull(world.get(), "The reference to the world was lost");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentSideEffectSet(SideEffectSet sideEffectSet) {
|
||||
this.sideEffectSet = sideEffectSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelChunk getChunk(int x, int z) {
|
||||
return getWorld().getChunk(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.world.level.block.state.BlockState toNative(BlockState state) {
|
||||
int stateId = BlockStateIdAccess.getBlockStateId(state);
|
||||
return BlockStateIdAccess.isValidInternalId(stateId)
|
||||
? Block.stateById(stateId)
|
||||
: ((CraftBlockData) BukkitAdapter.adapt(state)).getState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.world.level.block.state.BlockState getBlockState(LevelChunk chunk, BlockPos position) {
|
||||
return chunk.getBlockState(position);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public net.minecraft.world.level.block.state.BlockState setBlockState(LevelChunk chunk, BlockPos position, net.minecraft.world.level.block.state.BlockState state) {
|
||||
return chunk.setBlockState(position, state, false, this.sideEffectSet.shouldApply(SideEffect.UPDATE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.world.level.block.state.BlockState getValidBlockForPosition(net.minecraft.world.level.block.state.BlockState block, BlockPos position) {
|
||||
return Block.updateFromNeighbourShapes(block, getWorld(), position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockPos getPosition(int x, int y, int z) {
|
||||
return new BlockPos(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLightingForBlock(BlockPos position) {
|
||||
getWorld().getChunkSource().getLightEngine().checkBlock(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateTileEntity(BlockPos position, LinCompoundTag tag) {
|
||||
// We will assume that the tile entity was created for us
|
||||
BlockEntity tileEntity = getWorld().getBlockEntity(position);
|
||||
if (tileEntity == null) {
|
||||
return false;
|
||||
}
|
||||
Tag nativeTag = adapter.fromNative(tag);
|
||||
PaperweightAdapter.readTagIntoTileEntity((net.minecraft.nbt.CompoundTag) nativeTag, tileEntity);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyBlockUpdate(LevelChunk chunk, BlockPos position, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState) {
|
||||
if (chunk.getSections()[getWorld().getSectionIndex(position.getY())] != null) {
|
||||
getWorld().sendBlockUpdated(position, oldState, newState, UPDATE | NOTIFY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkTicking(LevelChunk chunk) {
|
||||
return chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markBlockChanged(LevelChunk chunk, BlockPos position) {
|
||||
if (chunk.getSections()[getWorld().getSectionIndex(position.getY())] != null) {
|
||||
getWorld().getChunkSource().blockChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyNeighbors(BlockPos pos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState) {
|
||||
ServerLevel world = getWorld();
|
||||
if (sideEffectSet.shouldApply(SideEffect.EVENTS)) {
|
||||
world.updateNeighborsAt(pos, oldState.getBlock());
|
||||
} else {
|
||||
// When we don't want events, manually run the physics without them.
|
||||
Block block = oldState.getBlock();
|
||||
fireNeighborChanged(pos, world, block, pos.west());
|
||||
fireNeighborChanged(pos, world, block, pos.east());
|
||||
fireNeighborChanged(pos, world, block, pos.below());
|
||||
fireNeighborChanged(pos, world, block, pos.above());
|
||||
fireNeighborChanged(pos, world, block, pos.north());
|
||||
fireNeighborChanged(pos, world, block, pos.south());
|
||||
}
|
||||
if (newState.hasAnalogOutputSignal()) {
|
||||
world.updateNeighbourForOutputSignal(pos, newState.getBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBlock(BlockPos pos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState) {
|
||||
ServerLevel world = getWorld();
|
||||
newState.onPlace(world, pos, oldState, false);
|
||||
}
|
||||
|
||||
// Not sure why neighborChanged is deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
private void fireNeighborChanged(BlockPos pos, ServerLevel world, Block block, BlockPos neighborPos) {
|
||||
world.getBlockState(neighborPos).neighborChanged(world, neighborPos, block, pos, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNeighbors(BlockPos pos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState, int recursionLimit) {
|
||||
ServerLevel world = getWorld();
|
||||
oldState.updateIndirectNeighbourShapes(world, pos, NOTIFY, recursionLimit);
|
||||
if (sideEffectSet.shouldApply(SideEffect.EVENTS)) {
|
||||
CraftWorld craftWorld = world.getWorld();
|
||||
BlockPhysicsEvent event = new BlockPhysicsEvent(craftWorld.getBlockAt(pos.getX(), pos.getY(), pos.getZ()), CraftBlockData.fromData(newState));
|
||||
world.getCraftServer().getPluginManager().callEvent(event);
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
newState.updateNeighbourShapes(world, pos, NOTIFY, recursionLimit);
|
||||
newState.updateIndirectNeighbourShapes(world, pos, NOTIFY, recursionLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockStateChange(BlockPos pos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState) {
|
||||
getWorld().onBlockStateChange(pos, oldState, newState);
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@
|
||||
import com.sk89q.worldedit.util.lifecycle.Lifecycled;
|
||||
import com.sk89q.worldedit.world.DataFixer;
|
||||
import com.sk89q.worldedit.world.registry.Registries;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
@ -239,6 +240,14 @@ public Set<SideEffect> getSupportedSideEffects() {
|
||||
return SUPPORTED_SIDE_EFFECTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTickCount() {
|
||||
if (PaperLib.isPaper()) {
|
||||
return Bukkit.getCurrentTick();
|
||||
}
|
||||
return super.getTickCount();
|
||||
}
|
||||
|
||||
public void unregisterCommands() {
|
||||
dynamicCommands.unregisterCommands();
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
import com.sk89q.worldedit.entity.Player;
|
||||
import com.sk89q.worldedit.event.platform.SessionIdleEvent;
|
||||
import com.sk89q.worldedit.extension.platform.Actor;
|
||||
import com.sk89q.worldedit.internal.event.InteractionDebouncer;
|
||||
import com.sk89q.worldedit.util.Direction;
|
||||
import com.sk89q.worldedit.util.Location;
|
||||
import com.sk89q.worldedit.world.World;
|
||||
@ -52,6 +53,7 @@
|
||||
public class WorldEditListener implements Listener {
|
||||
|
||||
private final WorldEditPlugin plugin;
|
||||
private final InteractionDebouncer debouncer;
|
||||
|
||||
/**
|
||||
* Construct the object.
|
||||
@ -60,6 +62,7 @@ public class WorldEditListener implements Listener {
|
||||
*/
|
||||
public WorldEditListener(WorldEditPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
debouncer = new InteractionDebouncer(plugin.getInternalPlatform());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
@ -93,62 +96,59 @@ public void onPlayerCommandSend(PlayerCommandSendEvent event) {
|
||||
*/
|
||||
@EventHandler
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (!plugin.getInternalPlatform().isHookingEvents()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.useItemInHand() == Result.DENY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getHand() == EquipmentSlot.OFF_HAND) {
|
||||
if (!plugin.getInternalPlatform().isHookingEvents()
|
||||
|| event.useItemInHand() == Result.DENY
|
||||
|| event.getHand() == EquipmentSlot.OFF_HAND
|
||||
|| event.getAction() == Action.PHYSICAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Player player = plugin.wrapPlayer(event.getPlayer());
|
||||
|
||||
if (event.getAction() != Action.LEFT_CLICK_BLOCK) {
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
if (previousResult.get()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final World world = player.getWorld();
|
||||
final WorldEdit we = plugin.getWorldEdit();
|
||||
final Direction direction = BukkitAdapter.adapt(event.getBlockFace());
|
||||
final Block clickedBlock = event.getClickedBlock();
|
||||
final Location pos = clickedBlock == null ? null : new Location(world, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ());
|
||||
|
||||
Action action = event.getAction();
|
||||
if (action == Action.LEFT_CLICK_BLOCK) {
|
||||
final Block clickedBlock = event.getClickedBlock();
|
||||
final Location pos = new Location(world, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ());
|
||||
boolean result = false;
|
||||
switch (event.getAction()) {
|
||||
case LEFT_CLICK_BLOCK:
|
||||
result = we.handleBlockLeftClick(player, pos, direction) || we.handleArmSwing(player);
|
||||
break;
|
||||
case LEFT_CLICK_AIR:
|
||||
result = we.handleArmSwing(player);
|
||||
break;
|
||||
case RIGHT_CLICK_BLOCK:
|
||||
result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player);
|
||||
break;
|
||||
case RIGHT_CLICK_AIR:
|
||||
result = we.handleRightClick(player);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (we.handleBlockLeftClick(player, pos, direction)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
if (we.handleArmSwing(player)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
} else if (action == Action.LEFT_CLICK_AIR) {
|
||||
|
||||
if (we.handleArmSwing(player)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
} else if (action == Action.RIGHT_CLICK_BLOCK) {
|
||||
final Block clickedBlock = event.getClickedBlock();
|
||||
final Location pos = new Location(world, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ());
|
||||
|
||||
if (we.handleBlockRightClick(player, pos, direction)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
if (we.handleRightClick(player)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
} else if (action == Action.RIGHT_CLICK_AIR) {
|
||||
if (we.handleRightClick(player)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
debouncer.setLastInteraction(player, result);
|
||||
if (result) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
debouncer.clearInteraction(plugin.wrapPlayer(event.getPlayer()));
|
||||
|
||||
plugin.getWorldEdit().getEventBus().post(new SessionIdleEvent(new BukkitPlayer.SessionKeyImpl(event.getPlayer())));
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +97,8 @@ public YAMLProcessor(File file, boolean writeDefaults, YAMLFormat format) {
|
||||
representer.setDefaultFlowStyle(format.getStyle());
|
||||
LoaderOptions loaderOptions = new LoaderOptions();
|
||||
try {
|
||||
int yamlAliasLimit = Integer.getInteger("worldedit.yaml.aliasLimit", 50);
|
||||
loaderOptions.setMaxAliasesForCollections(yamlAliasLimit);
|
||||
// 64 MB default
|
||||
int yamlCodePointLimit = Integer.getInteger("worldedit.yaml.codePointLimit", 64 * 1024 * 1024);
|
||||
loaderOptions.setCodePointLimit(yamlCodePointLimit);
|
||||
@ -104,7 +106,7 @@ public YAMLProcessor(File file, boolean writeDefaults, YAMLFormat format) {
|
||||
// pre-1.32 snakeyaml
|
||||
}
|
||||
|
||||
yaml = new Yaml(new SafeConstructor(new LoaderOptions()), representer, dumperOptions, loaderOptions);
|
||||
yaml = new Yaml(new SafeConstructor(loaderOptions), representer, dumperOptions, loaderOptions);
|
||||
|
||||
this.file = file;
|
||||
}
|
||||
|
@ -1044,10 +1044,17 @@ public int fillXZ(BlockVector3 origin, Pattern pattern, double radius, int depth
|
||||
checkArgument(radius >= 0, "radius >= 0");
|
||||
checkArgument(depth >= 1, "depth >= 1");
|
||||
|
||||
// Avoid int overflow (negative coordinate space allows for overflow back round to positive if the depth is large enough).
|
||||
// Depth is always 1 or greater, thus the lower bound should always be <= origin y.
|
||||
int lowerBound = origin.getBlockY() - depth + 1;
|
||||
if (lowerBound > origin.getBlockY()) {
|
||||
lowerBound = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
MaskIntersection mask = new MaskIntersection(
|
||||
new RegionMask(new EllipsoidRegion(null, origin, Vector3.at(radius, radius, radius))),
|
||||
new BoundedHeightMask(
|
||||
Math.max(origin.getBlockY() - depth + 1, getWorld().getMinY()),
|
||||
Math.max(lowerBound, getWorld().getMinY()),
|
||||
Math.min(getWorld().getMaxY(), origin.getBlockY())),
|
||||
Masks.negate(new ExistingBlockMask(this)));
|
||||
|
||||
|
@ -84,4 +84,8 @@ public void reload() {
|
||||
getTranslationManager().reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTickCount() {
|
||||
return System.nanoTime() / 50_000_000;
|
||||
}
|
||||
}
|
||||
|
@ -222,4 +222,12 @@ default String getId() {
|
||||
* @return A set of supported side effects
|
||||
*/
|
||||
Set<SideEffect> getSupportedSideEffects();
|
||||
|
||||
/**
|
||||
* Get the number of ticks since the server started.
|
||||
* On some platforms this value may be an approximation based on the JVM run time.
|
||||
*
|
||||
* @return The number of ticks since the server started.
|
||||
*/
|
||||
long getTickCount();
|
||||
}
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.internal.event;
|
||||
|
||||
import com.sk89q.worldedit.extension.platform.Platform;
|
||||
import com.sk89q.worldedit.util.Identifiable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class InteractionDebouncer {
|
||||
private final Platform platform;
|
||||
private final Map<UUID, Interaction> lastInteractions = new HashMap<>();
|
||||
|
||||
public InteractionDebouncer(Platform platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public void clearInteraction(Identifiable player) {
|
||||
lastInteractions.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
public void setLastInteraction(Identifiable player, boolean result) {
|
||||
lastInteractions.put(player.getUniqueId(), new Interaction(platform.getTickCount(), result));
|
||||
}
|
||||
|
||||
public Optional<Boolean> getDuplicateInteractionResult(Identifiable player) {
|
||||
Interaction last = lastInteractions.get(player.getUniqueId());
|
||||
if (last == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
long now = platform.getTickCount();
|
||||
if (now - last.tick <= 1) {
|
||||
return Optional.of(last.result);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static class Interaction {
|
||||
public final long tick;
|
||||
public final boolean result;
|
||||
|
||||
public Interaction(long tick, boolean result) {
|
||||
this.tick = tick;
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import net.fabricmc.loom.api.LoomGradleExtensionAPI
|
||||
import net.fabricmc.loom.configuration.FabricApiExtension
|
||||
import net.fabricmc.loom.task.RemapJarTask
|
||||
|
||||
buildscript {
|
||||
@ -21,8 +22,8 @@
|
||||
apply(plugin = "fabric-loom")
|
||||
apply(plugin = "java-library")
|
||||
|
||||
val minecraftVersion = "1.20"
|
||||
val loaderVersion = "0.14.21"
|
||||
val minecraftVersion = "1.20.2"
|
||||
val loaderVersion = "0.14.22"
|
||||
|
||||
val fabricApiConfiguration: Configuration = configurations.create("fabricApi")
|
||||
|
||||
@ -52,52 +53,20 @@
|
||||
"mappings"(project.the<LoomGradleExtensionAPI>().officialMojangMappings())
|
||||
"modImplementation"("net.fabricmc:fabric-loader:$loaderVersion")
|
||||
|
||||
// [1] declare fabric-api dependency...
|
||||
"fabricApi"("net.fabricmc.fabric-api:fabric-api:0.83.0+1.20")
|
||||
|
||||
// [2] Load the API dependencies from the fabric mod json...
|
||||
// [1] Load the API dependencies from the fabric mod json...
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val fabricModJson = file("src/main/resources/fabric.mod.json").bufferedReader().use {
|
||||
groovy.json.JsonSlurper().parse(it) as Map<String, Map<String, *>>
|
||||
}
|
||||
val wantedDependencies = (fabricModJson["depends"] ?: error("no depends in fabric.mod.json")).keys
|
||||
.filter { it == "fabric-api-base" || it.contains(Regex("v\\d$")) }
|
||||
.map { "net.fabricmc.fabric-api:$it" }
|
||||
.toSet()
|
||||
logger.lifecycle("Looking for these dependencies:")
|
||||
// [2] Request the matching dependency from fabric-loom
|
||||
for (wantedDependency in wantedDependencies) {
|
||||
logger.lifecycle(wantedDependency)
|
||||
}
|
||||
// [3] and now we resolve it to pick out what we want :D
|
||||
val fabricApiDependencies = fabricApiConfiguration.incoming.resolutionResult.allDependencies
|
||||
.onEach {
|
||||
if (it is UnresolvedDependencyResult) {
|
||||
throw kotlin.IllegalStateException("Failed to resolve Fabric API", it.failure)
|
||||
}
|
||||
}
|
||||
.filterIsInstance<ResolvedDependencyResult>()
|
||||
// pick out transitive dependencies
|
||||
.flatMap {
|
||||
it.selected.dependencies
|
||||
}
|
||||
// grab the requested versions
|
||||
.map { it.requested }
|
||||
.filterIsInstance<ModuleComponentSelector>()
|
||||
// map to standard notation
|
||||
.associateByTo(
|
||||
mutableMapOf(),
|
||||
keySelector = { "${it.group}:${it.module}" },
|
||||
valueTransform = { "${it.group}:${it.module}:${it.version}" }
|
||||
)
|
||||
fabricApiDependencies.keys.retainAll(wantedDependencies)
|
||||
// sanity check
|
||||
for (wantedDep in wantedDependencies) {
|
||||
check(wantedDep in fabricApiDependencies) { "Fabric API library $wantedDep is missing!" }
|
||||
}
|
||||
|
||||
fabricApiDependencies.values.forEach {
|
||||
"include"(it)
|
||||
"modImplementation"(it)
|
||||
val dep = project.the<FabricApiExtension>().module(wantedDependency, "0.89.1+1.20.2")
|
||||
"include"(dep)
|
||||
"modImplementation"(dep)
|
||||
}
|
||||
|
||||
// No need for this at runtime
|
||||
|
@ -218,6 +218,11 @@ public Set<SideEffect> getSupportedSideEffects() {
|
||||
: SUPPORTED_SIDE_EFFECTS_NO_MIXIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTickCount() {
|
||||
return FabricWorldEdit.LIFECYCLED_SERVER.valueOrThrow().getTickCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Actor> getConnectedUsers() {
|
||||
List<Actor> users = new ArrayList<>();
|
||||
|
@ -24,9 +24,7 @@
|
||||
import com.sk89q.worldedit.entity.BaseEntity;
|
||||
import com.sk89q.worldedit.extension.platform.AbstractPlayerActor;
|
||||
import com.sk89q.worldedit.extent.inventory.BlockBag;
|
||||
import com.sk89q.worldedit.fabric.internal.ExtendedPlayerEntity;
|
||||
import com.sk89q.worldedit.fabric.internal.NBTConverter;
|
||||
import com.sk89q.worldedit.fabric.mixin.AccessorClientboundBlockEntityDataPacket;
|
||||
import com.sk89q.worldedit.fabric.net.handler.WECUIPacketHandler;
|
||||
import com.sk89q.worldedit.internal.cui.CUIEvent;
|
||||
import com.sk89q.worldedit.math.BlockVector3;
|
||||
@ -48,6 +46,7 @@
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@ -141,7 +140,7 @@ public void dispatchCUIEvent(CUIEvent event) {
|
||||
|
||||
@Override
|
||||
public Locale getLocale() {
|
||||
return TextUtils.getLocaleByMinecraftTag(((ExtendedPlayerEntity) this.player).getLanguage());
|
||||
return TextUtils.getLocaleByMinecraftTag(this.player.clientInformation().language());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -244,10 +243,10 @@ public <B extends BlockStateHolder<B>> void sendFakeBlock(BlockVector3 pos, B bl
|
||||
if (block instanceof BaseBlock && block.getBlockType().equals(BlockTypes.STRUCTURE_BLOCK)) {
|
||||
final LinCompoundTag nbtData = ((BaseBlock) block).getNbt();
|
||||
if (nbtData != null) {
|
||||
player.connection.send(AccessorClientboundBlockEntityDataPacket.construct(
|
||||
new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()),
|
||||
BlockEntityType.STRUCTURE_BLOCK,
|
||||
NBTConverter.toNative(nbtData))
|
||||
player.connection.send(new ClientboundBlockEntityDataPacket(
|
||||
new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()),
|
||||
BlockEntityType.STRUCTURE_BLOCK,
|
||||
NBTConverter.toNative(nbtData))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,6 @@
|
||||
import com.sk89q.worldedit.fabric.internal.FabricServerLevelDelegateProxy;
|
||||
import com.sk89q.worldedit.fabric.internal.FabricWorldNativeAccess;
|
||||
import com.sk89q.worldedit.fabric.internal.NBTConverter;
|
||||
import com.sk89q.worldedit.fabric.mixin.AccessorDerivedLevelData;
|
||||
import com.sk89q.worldedit.fabric.mixin.AccessorPrimaryLevelData;
|
||||
import com.sk89q.worldedit.fabric.mixin.AccessorServerChunkCache;
|
||||
import com.sk89q.worldedit.function.mask.AbstractExtentMask;
|
||||
import com.sk89q.worldedit.function.mask.Mask;
|
||||
import com.sk89q.worldedit.function.mask.Mask2D;
|
||||
@ -104,8 +101,10 @@
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.storage.PrimaryLevelData;
|
||||
import net.minecraft.world.level.storage.ServerLevelData;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
@ -333,20 +332,14 @@ private void doRegen(Region region, Extent extent, RegenOptions options) throws
|
||||
LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir);
|
||||
try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("WorldEditTempGen")) {
|
||||
ServerLevel originalWorld = (ServerLevel) getWorld();
|
||||
AccessorPrimaryLevelData levelProperties;
|
||||
if (originalWorld.getLevelData() instanceof AccessorDerivedLevelData derivedLevelData) {
|
||||
levelProperties = (AccessorPrimaryLevelData) derivedLevelData.getWrapped();
|
||||
} else {
|
||||
levelProperties = (AccessorPrimaryLevelData) originalWorld.getLevelData();
|
||||
}
|
||||
PrimaryLevelData levelProperties = getPrimaryLevelData(originalWorld.getLevelData());
|
||||
WorldOptions originalOpts = levelProperties.worldGenOptions();
|
||||
|
||||
long seed = options.getSeed().orElse(originalWorld.getSeed());
|
||||
WorldOptions newOpts = options.getSeed().isPresent()
|
||||
levelProperties.worldOptions = options.getSeed().isPresent()
|
||||
? originalOpts.withSeed(OptionalLong.of(seed))
|
||||
: originalOpts;
|
||||
|
||||
levelProperties.setWorldOptions(newOpts);
|
||||
ResourceKey<Level> worldRegKey = originalWorld.dimension();
|
||||
try (ServerLevel serverWorld = new ServerLevel(
|
||||
originalWorld.getServer(), Util.backgroundExecutor(), session,
|
||||
@ -372,19 +365,29 @@ private void doRegen(Region region, Extent extent, RegenOptions options) throws
|
||||
Thread.yield();
|
||||
}
|
||||
} finally {
|
||||
levelProperties.setWorldOptions(originalOpts);
|
||||
levelProperties.worldOptions = originalOpts;
|
||||
}
|
||||
} finally {
|
||||
SafeFiles.tryHardToDeleteDir(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static PrimaryLevelData getPrimaryLevelData(LevelData levelData) {
|
||||
if (levelData instanceof DerivedLevelData derivedLevelData) {
|
||||
return getPrimaryLevelData(derivedLevelData.wrapped);
|
||||
} else if (levelData instanceof PrimaryLevelData primaryLevelData) {
|
||||
return primaryLevelData;
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown level data type: " + levelData.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld,
|
||||
RegenOptions options) throws WorldEditException {
|
||||
List<CompletableFuture<ChunkAccess>> chunkLoadings = submitChunkLoadTasks(region, serverWorld);
|
||||
|
||||
// drive executor until loading finishes
|
||||
((AccessorServerChunkCache) serverWorld.getChunkSource()).getMainThreadProcessor()
|
||||
serverWorld.getChunkSource().mainThreadProcessor
|
||||
.managedBlock(() -> {
|
||||
// bail out early if a future fails
|
||||
if (chunkLoadings.stream().anyMatch(ftr ->
|
||||
@ -422,12 +425,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld
|
||||
}
|
||||
|
||||
private List<CompletableFuture<ChunkAccess>> submitChunkLoadTasks(Region region, ServerLevel world) {
|
||||
AccessorServerChunkCache chunkManager = (AccessorServerChunkCache) world.getChunkSource();
|
||||
List<CompletableFuture<ChunkAccess>> chunkLoadings = new ArrayList<>();
|
||||
// Pre-gen all the chunks
|
||||
for (BlockVector2 chunk : region.getChunks()) {
|
||||
chunkLoadings.add(
|
||||
chunkManager.callGetChunkFuture(chunk.getX(), chunk.getZ(), ChunkStatus.FEATURES, true)
|
||||
world.getChunkSource().getChunkFuture(chunk.getX(), chunk.getZ(), ChunkStatus.FEATURES, true)
|
||||
.thenApply(either -> either.left().orElse(null))
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,7 @@
|
||||
import com.sk89q.worldedit.extension.platform.PlatformManager;
|
||||
import com.sk89q.worldedit.fabric.net.handler.WECUIPacketHandler;
|
||||
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
|
||||
import com.sk89q.worldedit.internal.event.InteractionDebouncer;
|
||||
import com.sk89q.worldedit.internal.util.LogManagerCompat;
|
||||
import com.sk89q.worldedit.util.Location;
|
||||
import com.sk89q.worldedit.util.lifecycle.Lifecycled;
|
||||
@ -87,6 +88,7 @@
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -120,6 +122,7 @@ public static <T> Registry<T> getRegistry(ResourceKey<Registry<T>> key) {
|
||||
|
||||
public static FabricWorldEdit inst;
|
||||
|
||||
private InteractionDebouncer debouncer;
|
||||
private FabricPlatform platform;
|
||||
private FabricConfiguration config;
|
||||
private Path workingDir;
|
||||
@ -146,6 +149,7 @@ public void onInitialize() {
|
||||
}
|
||||
}
|
||||
this.platform = new FabricPlatform(this);
|
||||
debouncer = new InteractionDebouncer(platform);
|
||||
|
||||
WorldEdit.getInstance().getPlatformManager().register(platform);
|
||||
|
||||
@ -284,16 +288,16 @@ private void onStopServer(MinecraftServer minecraftServer) {
|
||||
WorldEdit.getInstance().getEventBus().post(new PlatformUnreadyEvent(platform));
|
||||
}
|
||||
|
||||
private boolean shouldSkip() {
|
||||
if (platform == null) {
|
||||
return true;
|
||||
}
|
||||
private boolean skipEvents() {
|
||||
return platform == null || !platform.isHookingEvents();
|
||||
}
|
||||
|
||||
return !platform.isHookingEvents(); // We have to be told to catch these events
|
||||
private boolean skipInteractionEvent(Player player, InteractionHand hand) {
|
||||
return skipEvents() || hand != InteractionHand.MAIN_HAND || player.level().isClientSide || !(player instanceof ServerPlayer);
|
||||
}
|
||||
|
||||
private InteractionResult onLeftClickBlock(Player playerEntity, Level world, InteractionHand hand, BlockPos blockPos, Direction direction) {
|
||||
if (shouldSkip() || hand == InteractionHand.OFF_HAND || world.isClientSide) {
|
||||
if (skipInteractionEvent(playerEntity, hand)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
@ -307,19 +311,14 @@ private InteractionResult onLeftClickBlock(Player playerEntity, Level world, Int
|
||||
);
|
||||
com.sk89q.worldedit.util.Direction weDirection = FabricAdapter.adaptEnumFacing(direction);
|
||||
|
||||
if (we.handleBlockLeftClick(player, pos, weDirection)) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
boolean result = we.handleBlockLeftClick(player, pos, weDirection) || we.handleArmSwing(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (we.handleArmSwing(player)) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
return result ? InteractionResult.SUCCESS : InteractionResult.PASS;
|
||||
}
|
||||
|
||||
private InteractionResult onRightClickBlock(Player playerEntity, Level world, InteractionHand hand, BlockHitResult blockHitResult) {
|
||||
if (shouldSkip() || hand == InteractionHand.OFF_HAND || world.isClientSide) {
|
||||
if (skipInteractionEvent(playerEntity, hand)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
@ -333,36 +332,52 @@ private InteractionResult onRightClickBlock(Player playerEntity, Level world, In
|
||||
);
|
||||
com.sk89q.worldedit.util.Direction direction = FabricAdapter.adaptEnumFacing(blockHitResult.getDirection());
|
||||
|
||||
if (we.handleBlockRightClick(player, pos, direction)) {
|
||||
return InteractionResult.SUCCESS;
|
||||
boolean result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
return result ? InteractionResult.SUCCESS : InteractionResult.PASS;
|
||||
}
|
||||
|
||||
public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) {
|
||||
if (skipInteractionEvent(playerEntity, hand)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (we.handleRightClick(player)) {
|
||||
return InteractionResult.SUCCESS;
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
FabricPlayer player = adaptPlayer(playerEntity);
|
||||
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
boolean result = we.handleArmSwing(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
}
|
||||
|
||||
private InteractionResultHolder<ItemStack> onRightClickAir(Player playerEntity, Level world, InteractionHand hand) {
|
||||
ItemStack stackInHand = playerEntity.getItemInHand(hand);
|
||||
if (shouldSkip() || hand == InteractionHand.OFF_HAND || world.isClientSide) {
|
||||
if (skipInteractionEvent(playerEntity, hand)) {
|
||||
return InteractionResultHolder.pass(stackInHand);
|
||||
}
|
||||
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity);
|
||||
|
||||
if (we.handleRightClick(player)) {
|
||||
return InteractionResultHolder.success(stackInHand);
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
return previousResult.get() ? InteractionResultHolder.success(stackInHand) : InteractionResultHolder.pass(stackInHand);
|
||||
}
|
||||
|
||||
return InteractionResultHolder.pass(stackInHand);
|
||||
boolean result = we.handleRightClick(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
return result ? InteractionResultHolder.success(stackInHand) : InteractionResultHolder.pass(stackInHand);
|
||||
}
|
||||
|
||||
// TODO Pass empty left click to server
|
||||
|
||||
private void onPlayerDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server) {
|
||||
debouncer.clearInteraction(adaptPlayer(handler.player));
|
||||
|
||||
WorldEdit.getInstance().getEventBus()
|
||||
.post(new SessionIdleEvent(new FabricPlayer.SessionKeyImpl(handler.player)));
|
||||
}
|
||||
|
@ -21,18 +21,24 @@
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ClientInformation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stat;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.player.ChatVisiblity;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class WorldEditFakePlayer extends ServerPlayer {
|
||||
private static final GameProfile FAKE_WORLDEDIT_PROFILE = new GameProfile(UUID.nameUUIDFromBytes("worldedit".getBytes()), "[WorldEdit]");
|
||||
private static final ClientInformation FAKE_CLIENT_INFO = new ClientInformation(
|
||||
"en_US", 16, ChatVisiblity.FULL, true, 0, HumanoidArm.LEFT, false, false
|
||||
);
|
||||
|
||||
public WorldEditFakePlayer(ServerLevel world) {
|
||||
super(world.getServer(), world, FAKE_WORLDEDIT_PROFILE);
|
||||
super(world.getServer(), world, FAKE_WORLDEDIT_PROFILE, FAKE_CLIENT_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.internal;
|
||||
|
||||
public interface ExtendedPlayerEntity {
|
||||
|
||||
String getLanguage();
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.mixin;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
@Mixin(ClientboundBlockEntityDataPacket.class)
|
||||
public interface AccessorClientboundBlockEntityDataPacket {
|
||||
@Invoker("<init>")
|
||||
static ClientboundBlockEntityDataPacket construct(BlockPos blockPos, BlockEntityType<?> blockEntityType, CompoundTag compoundTag) {
|
||||
throw new AssertionError("This is replaced by Mixin to call the constructor.");
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.mixin;
|
||||
|
||||
import net.minecraft.world.level.levelgen.WorldOptions;
|
||||
import net.minecraft.world.level.storage.PrimaryLevelData;
|
||||
import net.minecraft.world.level.storage.WorldData;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Mutable;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(PrimaryLevelData.class)
|
||||
public interface AccessorPrimaryLevelData extends WorldData {
|
||||
|
||||
@Accessor
|
||||
@Mutable
|
||||
void setWorldOptions(WorldOptions worldOptions);
|
||||
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.mixin;
|
||||
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import net.minecraft.server.level.ChunkHolder;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkStatus;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Mixin(ServerChunkCache.class)
|
||||
public interface AccessorServerChunkCache {
|
||||
|
||||
@Invoker
|
||||
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> callGetChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
|
||||
|
||||
@Accessor
|
||||
ServerChunkCache.MainThreadExecutor getMainThreadProcessor();
|
||||
|
||||
}
|
@ -34,6 +34,7 @@
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.levelgen.blending.BlendingData;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.Slice;
|
||||
@ -42,12 +43,14 @@
|
||||
|
||||
@Mixin(LevelChunk.class)
|
||||
public abstract class MixinLevelChunkSetBlockHook extends ChunkAccess implements ExtendedChunk {
|
||||
@Unique
|
||||
private boolean shouldUpdate = true;
|
||||
|
||||
public MixinLevelChunkSetBlockHook(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, Registry<Biome> registry, long l, @org.jetbrains.annotations.Nullable LevelChunkSection[] levelChunkSections, @org.jetbrains.annotations.Nullable BlendingData blendingData) {
|
||||
super(chunkPos, upgradeData, levelHeightAccessor, registry, l, levelChunkSections, blendingData);
|
||||
}
|
||||
|
||||
@Unique
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockState setBlockState(BlockPos pos, BlockState state, boolean moved, boolean update) {
|
||||
|
@ -28,6 +28,7 @@
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@ -40,11 +41,13 @@ public abstract class MixinMinecraftServer implements Watchdog, ExtendedMinecraf
|
||||
@Shadow
|
||||
protected LevelStorageSource.LevelStorageAccess storageSource;
|
||||
|
||||
@Unique
|
||||
@Override
|
||||
public void tick() {
|
||||
nextTickTime = Util.getMillis();
|
||||
}
|
||||
|
||||
@Unique
|
||||
@Override
|
||||
public Path getStoragePath(Level world) {
|
||||
return storageSource.getDimensionPath(world.dimension());
|
||||
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.mixin;
|
||||
|
||||
import com.sk89q.worldedit.fabric.FabricWorldEdit;
|
||||
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
|
||||
import net.minecraft.network.protocol.game.ServerboundSwingPacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ServerGamePacketListenerImpl.class)
|
||||
public class MixinServerGamePacketListenerImpl {
|
||||
@Shadow
|
||||
public ServerPlayer player;
|
||||
|
||||
@Unique
|
||||
private int ignoreSwingPackets;
|
||||
|
||||
@Inject(method = "handleAnimate", at = @At("HEAD"))
|
||||
private void onAnimate(ServerboundSwingPacket packet, CallbackInfo ci) {
|
||||
if (!this.player.gameMode.isDestroyingBlock) {
|
||||
if (this.ignoreSwingPackets > 0) {
|
||||
this.ignoreSwingPackets--;
|
||||
} else if (FabricWorldEdit.inst != null) {
|
||||
FabricWorldEdit.inst.onLeftClickAir(this.player, packet.getHand());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "handlePlayerAction", at = @At("HEAD"))
|
||||
private void onAction(ServerboundPlayerActionPacket packet, CallbackInfo ci) {
|
||||
switch (packet.getAction()) {
|
||||
case DROP_ITEM, DROP_ALL_ITEMS, START_DESTROY_BLOCK -> this.ignoreSwingPackets++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.mixin;
|
||||
|
||||
import com.sk89q.worldedit.fabric.internal.ExtendedPlayerEntity;
|
||||
import net.minecraft.network.protocol.game.ServerboundClientInformationPacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
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;
|
||||
|
||||
@Mixin(ServerPlayer.class)
|
||||
public abstract class MixinServerPlayer implements ExtendedPlayerEntity {
|
||||
|
||||
private String language = "en_us";
|
||||
|
||||
@Inject(method = "updateOptions", at = @At(value = "HEAD"))
|
||||
public void updateOptions(ServerboundClientInformationPacket clientSettingsC2SPacket,
|
||||
CallbackInfo callbackInfo) {
|
||||
this.language = clientSettingsC2SPacket.language();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
}
|
@ -3,13 +3,9 @@
|
||||
"package": "com.sk89q.worldedit.fabric.mixin",
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"mixins": [
|
||||
"AccessorClientboundBlockEntityDataPacket",
|
||||
"AccessorPrimaryLevelData",
|
||||
"AccessorDerivedLevelData",
|
||||
"AccessorServerChunkCache",
|
||||
"MixinLevelChunkSetBlockHook",
|
||||
"MixinMinecraftServer",
|
||||
"MixinServerPlayer"
|
||||
"MixinServerGamePacketListenerImpl"
|
||||
],
|
||||
"plugin": "com.sk89q.worldedit.fabric.internal.MixinConfigPlugin",
|
||||
"server": [
|
||||
|
@ -1,3 +1,15 @@
|
||||
accessWidener v2 named
|
||||
|
||||
accessible class net/minecraft/server/level/ServerChunkCache$MainThreadExecutor
|
||||
accessible field net/minecraft/commands/CommandSourceStack source Lnet/minecraft/commands/CommandSource;
|
||||
accessible field net/minecraft/server/level/ServerChunkCache mainThreadProcessor Lnet/minecraft/server/level/ServerChunkCache$MainThreadExecutor;
|
||||
|
||||
accessible field net/minecraft/commands/CommandSourceStack source Lnet/minecraft/commands/CommandSource;
|
||||
|
||||
accessible field net/minecraft/server/level/ServerPlayerGameMode isDestroyingBlock Z
|
||||
|
||||
accessible field net/minecraft/world/level/storage/DerivedLevelData wrapped Lnet/minecraft/world/level/storage/ServerLevelData;
|
||||
|
||||
accessible field net/minecraft/world/level/storage/PrimaryLevelData worldOptions Lnet/minecraft/world/level/levelgen/WorldOptions;
|
||||
mutable field net/minecraft/world/level/storage/PrimaryLevelData worldOptions Lnet/minecraft/world/level/levelgen/WorldOptions;
|
||||
|
||||
accessible method net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket <init> (Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/nbt/CompoundTag;)V
|
||||
|
@ -12,11 +12,11 @@
|
||||
applyPlatformAndCoreConfiguration(javaRelease = 17)
|
||||
applyShadowConfiguration()
|
||||
|
||||
val minecraftVersion = "1.20"
|
||||
val minecraftVersion = "1.20.2"
|
||||
val nextMajorMinecraftVersion: String = minecraftVersion.split('.').let { (useless, major) ->
|
||||
"$useless.${major.toInt() + 1}"
|
||||
}
|
||||
val forgeVersion = "46.0.1"
|
||||
val forgeVersion = "48.0.1"
|
||||
|
||||
val apiClasspath = configurations.create("apiClasspath") {
|
||||
isCanBeResolved = true
|
||||
|
@ -63,18 +63,14 @@
|
||||
class ForgePlatform extends AbstractPlatform implements MultiUserPlatform {
|
||||
|
||||
private final ForgeWorldEdit mod;
|
||||
private final MinecraftServer server;
|
||||
private final ForgeDataFixer dataFixer;
|
||||
private final @Nullable ForgeWatchdog watchdog;
|
||||
private @Nullable ForgeWatchdog watchdog;
|
||||
private boolean hookingEvents = false;
|
||||
private final ResourceLoader resourceLoader = new ForgeResourceLoader(WorldEdit.getInstance());
|
||||
|
||||
ForgePlatform(ForgeWorldEdit mod) {
|
||||
this.mod = mod;
|
||||
this.server = ServerLifecycleHooks.getCurrentServer();
|
||||
this.dataFixer = new ForgeDataFixer(getDataVersion());
|
||||
this.watchdog = server instanceof DedicatedServer
|
||||
? new ForgeWatchdog((DedicatedServer) server) : null;
|
||||
}
|
||||
|
||||
boolean isHookingEvents() {
|
||||
@ -120,12 +116,18 @@ public int schedule(long delay, long period, Runnable task) {
|
||||
@Override
|
||||
@Nullable
|
||||
public ForgeWatchdog getWatchdog() {
|
||||
if (watchdog == null) {
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
if (server instanceof DedicatedServer) {
|
||||
watchdog = new ForgeWatchdog((DedicatedServer) server);
|
||||
}
|
||||
}
|
||||
return watchdog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends World> getWorlds() {
|
||||
Iterable<ServerLevel> worlds = server.getAllLevels();
|
||||
Iterable<ServerLevel> worlds = ServerLifecycleHooks.getCurrentServer().getAllLevels();
|
||||
List<World> ret = new ArrayList<>();
|
||||
for (ServerLevel world : worlds) {
|
||||
ret.add(new ForgeWorld(world));
|
||||
@ -139,7 +141,7 @@ public Player matchPlayer(Player player) {
|
||||
if (player instanceof ForgePlayer) {
|
||||
return player;
|
||||
} else {
|
||||
ServerPlayer entity = server.getPlayerList().getPlayerByName(player.getName());
|
||||
ServerPlayer entity = ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayerByName(player.getName());
|
||||
return entity != null ? new ForgePlayer(entity) : null;
|
||||
}
|
||||
}
|
||||
@ -150,7 +152,7 @@ public World matchWorld(World world) {
|
||||
if (world instanceof ForgeWorld) {
|
||||
return world;
|
||||
} else {
|
||||
for (ServerLevel ws : server.getAllLevels()) {
|
||||
for (ServerLevel ws : ServerLifecycleHooks.getCurrentServer().getAllLevels()) {
|
||||
if (((ServerLevelData) ws.getLevelData()).getLevelName().equals(world.getName())) {
|
||||
return new ForgeWorld(ws);
|
||||
}
|
||||
@ -162,6 +164,7 @@ public World matchWorld(World world) {
|
||||
|
||||
@Override
|
||||
public void registerCommands(CommandManager manager) {
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
@ -239,10 +242,15 @@ public Set<SideEffect> getSupportedSideEffects() {
|
||||
: SUPPORTED_SIDE_EFFECTS_NO_MIXIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTickCount() {
|
||||
return ServerLifecycleHooks.getCurrentServer().getTickCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Actor> getConnectedUsers() {
|
||||
List<Actor> users = new ArrayList<>();
|
||||
PlayerList scm = server.getPlayerList();
|
||||
PlayerList scm = ServerLifecycleHooks.getCurrentServer().getPlayerList();
|
||||
for (ServerPlayer entity : scm.getPlayers()) {
|
||||
if (entity != null) {
|
||||
users.add(new ForgePlayer(entity));
|
||||
|
@ -25,6 +25,7 @@
|
||||
import com.sk89q.worldedit.extension.platform.AbstractPlayerActor;
|
||||
import com.sk89q.worldedit.extent.inventory.BlockBag;
|
||||
import com.sk89q.worldedit.forge.internal.NBTConverter;
|
||||
import com.sk89q.worldedit.forge.net.handler.WECUIPacketHandler;
|
||||
import com.sk89q.worldedit.internal.cui.CUIEvent;
|
||||
import com.sk89q.worldedit.math.BlockVector3;
|
||||
import com.sk89q.worldedit.math.Vector3;
|
||||
@ -45,14 +46,11 @@
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.StructureBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import org.enginehub.linbus.tree.LinCompoundTag;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -134,11 +132,7 @@ public void dispatchCUIEvent(CUIEvent event) {
|
||||
send = send + "|" + StringUtil.joinString(params, "|");
|
||||
}
|
||||
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.copiedBuffer(send, StandardCharsets.UTF_8));
|
||||
ClientboundCustomPayloadPacket packet = new ClientboundCustomPayloadPacket(
|
||||
new ResourceLocation(ForgeWorldEdit.MOD_ID, ForgeWorldEdit.CUI_PLUGIN_CHANNEL),
|
||||
buffer
|
||||
);
|
||||
this.player.connection.send(packet);
|
||||
WECUIPacketHandler.send(this.player.connection.getConnection(), buffer);
|
||||
}
|
||||
|
||||
private void sendMessage(net.minecraft.network.chat.Component textComponent) {
|
||||
@ -251,12 +245,10 @@ public <B extends BlockStateHolder<B>> void sendFakeBlock(BlockVector3 pos, B bl
|
||||
if (block instanceof BaseBlock baseBlock && block.getBlockType().equals(BlockTypes.STRUCTURE_BLOCK)) {
|
||||
final LinCompoundTag nbtData = baseBlock.getNbt();
|
||||
if (nbtData != null) {
|
||||
player.connection.send(ClientboundBlockEntityDataPacket.create(
|
||||
new StructureBlockEntity(
|
||||
new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()),
|
||||
Blocks.STRUCTURE_BLOCK.defaultBlockState()
|
||||
),
|
||||
__ -> NBTConverter.toNative(nbtData)
|
||||
player.connection.send(new ClientboundBlockEntityDataPacket(
|
||||
new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()),
|
||||
BlockEntityType.STRUCTURE_BLOCK,
|
||||
NBTConverter.toNative(nbtData)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -31,10 +31,9 @@
|
||||
import com.sk89q.worldedit.extension.platform.Capability;
|
||||
import com.sk89q.worldedit.extension.platform.Platform;
|
||||
import com.sk89q.worldedit.extension.platform.PlatformManager;
|
||||
import com.sk89q.worldedit.forge.net.handler.InternalPacketHandler;
|
||||
import com.sk89q.worldedit.forge.net.handler.WECUIPacketHandler;
|
||||
import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage;
|
||||
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
|
||||
import com.sk89q.worldedit.internal.event.InteractionDebouncer;
|
||||
import com.sk89q.worldedit.internal.util.LogManagerCompat;
|
||||
import com.sk89q.worldedit.util.Direction;
|
||||
import com.sk89q.worldedit.util.Location;
|
||||
@ -48,6 +47,7 @@
|
||||
import com.sk89q.worldedit.world.item.ItemCategory;
|
||||
import com.sk89q.worldedit.world.item.ItemType;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
@ -58,13 +58,13 @@
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.CommandEvent;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent.LeftClickEmpty;
|
||||
import net.minecraftforge.event.server.ServerAboutToStartEvent;
|
||||
import net.minecraftforge.event.server.ServerStartedEvent;
|
||||
import net.minecraftforge.event.server.ServerStoppingEvent;
|
||||
@ -78,7 +78,6 @@
|
||||
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.fml.loading.FMLPaths;
|
||||
import net.minecraftforge.network.NetworkConstants;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.enginehub.piston.Command;
|
||||
@ -90,6 +89,7 @@
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
@ -113,6 +113,7 @@ public class ForgeWorldEdit {
|
||||
|
||||
public static ForgeWorldEdit inst;
|
||||
|
||||
private InteractionDebouncer debouncer;
|
||||
private ForgePlatform platform;
|
||||
private ForgeConfiguration config;
|
||||
private Path workingDir;
|
||||
@ -136,7 +137,7 @@ public ForgeWorldEdit() {
|
||||
ModLoadingContext.get(),
|
||||
IExtensionPoint.DisplayTest.class,
|
||||
(Supplier<?>) () -> new IExtensionPoint.DisplayTest(
|
||||
() -> NetworkConstants.IGNORESERVERONLY,
|
||||
() -> IExtensionPoint.DisplayTest.IGNORESERVERONLY,
|
||||
(a, b) -> true
|
||||
)
|
||||
);
|
||||
@ -161,13 +162,13 @@ private void init(FMLCommonSetupEvent event) {
|
||||
setupPlatform();
|
||||
|
||||
WECUIPacketHandler.init();
|
||||
InternalPacketHandler.init();
|
||||
|
||||
LOGGER.info("WorldEdit for Forge (version " + getInternalVersion() + ") is loaded");
|
||||
}
|
||||
|
||||
private void setupPlatform() {
|
||||
this.platform = new ForgePlatform(this);
|
||||
debouncer = new InteractionDebouncer(platform);
|
||||
|
||||
WorldEdit.getInstance().getPlatformManager().register(platform);
|
||||
|
||||
@ -293,61 +294,100 @@ public void serverStarted(ServerStartedEvent event) {
|
||||
WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent(platform));
|
||||
}
|
||||
|
||||
private boolean skipEvents() {
|
||||
return platform == null || !platform.isHookingEvents();
|
||||
}
|
||||
|
||||
private boolean skipInteractionEvent(Player player, InteractionHand hand) {
|
||||
return skipEvents() || hand != InteractionHand.MAIN_HAND || player.level().isClientSide || !(player instanceof ServerPlayer);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (platform == null) {
|
||||
public void onLeftClickBlock(PlayerInteractEvent.LeftClickBlock event) {
|
||||
if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem() == Event.Result.DENY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!platform.isHookingEvents()) {
|
||||
return; // We have to be told to catch these events
|
||||
}
|
||||
ServerPlayer playerEntity = (ServerPlayer) event.getEntity();
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
ForgePlayer player = adaptPlayer(playerEntity);
|
||||
ForgeWorld world = getWorld((ServerLevel) playerEntity.level());
|
||||
Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace());
|
||||
|
||||
if (event.getLevel().isClientSide && event instanceof LeftClickEmpty) {
|
||||
// catch LCE, pass it to server
|
||||
InternalPacketHandler.getHandler().sendToServer(LeftClickAirEventMessage.INSTANCE);
|
||||
BlockPos blockPos = event.getPos();
|
||||
Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
||||
|
||||
boolean result = we.handleBlockLeftClick(player, pos, direction) || we.handleArmSwing(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (result) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem() == Event.Result.DENY) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isLeftDeny = event instanceof PlayerInteractEvent.LeftClickBlock lcb
|
||||
&& lcb.getUseItem() == Event.Result.DENY;
|
||||
boolean isRightDeny = event instanceof PlayerInteractEvent.RightClickBlock rcb
|
||||
&& rcb.getUseItem() == Event.Result.DENY;
|
||||
if (isLeftDeny || isRightDeny || event.getEntity().level().isClientSide || event.getHand() == InteractionHand.OFF_HAND) {
|
||||
ServerPlayer playerEntity = (ServerPlayer) event.getEntity();
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
ForgePlayer player = adaptPlayer(playerEntity);
|
||||
ForgeWorld world = getWorld((ServerLevel) playerEntity.level());
|
||||
Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace());
|
||||
|
||||
BlockPos blockPos = event.getPos();
|
||||
Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
||||
|
||||
boolean result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (result) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) {
|
||||
if (skipInteractionEvent(playerEntity, hand)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
ForgePlayer player = adaptPlayer((ServerPlayer) event.getEntity());
|
||||
ForgeWorld world = getWorld((ServerLevel) event.getEntity().level());
|
||||
Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace());
|
||||
ForgePlayer player = adaptPlayer(playerEntity);
|
||||
|
||||
if (event instanceof PlayerInteractEvent.LeftClickEmpty) {
|
||||
we.handleArmSwing(player); // this event cannot be canceled
|
||||
} else if (event instanceof PlayerInteractEvent.LeftClickBlock) {
|
||||
Location pos = new Location(world, event.getPos().getX(), event.getPos().getY(), event.getPos().getZ());
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (we.handleBlockLeftClick(player, pos, direction)) {
|
||||
boolean result = we.handleArmSwing(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
|
||||
if (skipInteractionEvent(event.getEntity(), event.getHand())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServerPlayer playerEntity = (ServerPlayer) event.getEntity();
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
ForgePlayer player = adaptPlayer(playerEntity);
|
||||
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
if (previousResult.get()) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (we.handleArmSwing(player)) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
} else if (event instanceof PlayerInteractEvent.RightClickBlock) {
|
||||
Location pos = new Location(world, event.getPos().getX(), event.getPos().getY(), event.getPos().getZ());
|
||||
boolean result = we.handleRightClick(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (we.handleBlockRightClick(player, pos, direction)) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
if (we.handleRightClick(player)) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
} else if (event instanceof PlayerInteractEvent.RightClickItem) {
|
||||
if (we.handleRightClick(player)) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
if (result) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -370,6 +410,8 @@ public void onCommandEvent(CommandEvent event) throws CommandSyntaxException {
|
||||
@SubscribeEvent
|
||||
public void onPlayerLogOut(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
debouncer.clearInteraction(adaptPlayer(player));
|
||||
|
||||
WorldEdit.getInstance().getEventBus()
|
||||
.post(new SessionIdleEvent(new ForgePlayer.SessionKeyImpl(player)));
|
||||
}
|
||||
|
@ -20,8 +20,11 @@
|
||||
package com.sk89q.worldedit.forge;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.server.level.ClientInformation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.player.ChatVisiblity;
|
||||
import net.minecraftforge.common.util.FakePlayer;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
@ -29,11 +32,13 @@
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class WorldEditFakePlayer extends FakePlayer {
|
||||
|
||||
private static final GameProfile FAKE_GAME_PROFILE = new GameProfile(UUID.nameUUIDFromBytes("worldedit".getBytes()), "[WorldEdit]");
|
||||
private static final ClientInformation FAKE_CLIENT_INFO = new ClientInformation(
|
||||
"en_US", 16, ChatVisiblity.FULL, true, 0, HumanoidArm.LEFT, false, false
|
||||
);
|
||||
|
||||
public WorldEditFakePlayer(ServerLevel world) {
|
||||
super(world, FAKE_GAME_PROFILE);
|
||||
super(world, FAKE_GAME_PROFILE, FAKE_CLIENT_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,16 +17,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.fabric.mixin;
|
||||
package com.sk89q.worldedit.forge.mixin;
|
||||
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.ServerLevelData;
|
||||
import net.minecraft.server.level.ServerPlayerGameMode;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(DerivedLevelData.class)
|
||||
public interface AccessorDerivedLevelData extends ServerLevelData {
|
||||
@Mixin(ServerPlayerGameMode.class)
|
||||
public interface AccessorServerPlayerGameMode {
|
||||
|
||||
@Accessor
|
||||
ServerLevelData getWrapped();
|
||||
@Accessor("isDestroyingBlock")
|
||||
boolean isDestroyingBlock();
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.forge.mixin;
|
||||
|
||||
import com.sk89q.worldedit.forge.ForgeWorldEdit;
|
||||
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
|
||||
import net.minecraft.network.protocol.game.ServerboundSwingPacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ServerGamePacketListenerImpl.class)
|
||||
public class MixinServerGamePacketListenerImpl {
|
||||
@Shadow
|
||||
public ServerPlayer player;
|
||||
|
||||
private int ignoreSwingPackets;
|
||||
|
||||
@Inject(method = "handleAnimate", at = @At("HEAD"))
|
||||
private void onAnimate(ServerboundSwingPacket packet, CallbackInfo ci) {
|
||||
if (!((AccessorServerPlayerGameMode) this.player.gameMode).isDestroyingBlock()) {
|
||||
if (this.ignoreSwingPackets > 0) {
|
||||
this.ignoreSwingPackets--;
|
||||
} else if (ForgeWorldEdit.inst != null) {
|
||||
ForgeWorldEdit.inst.onLeftClickAir(this.player, packet.getHand());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "handlePlayerAction", at = @At("HEAD"))
|
||||
private void onAction(ServerboundPlayerActionPacket packet, CallbackInfo ci) {
|
||||
switch (packet.getAction()) {
|
||||
case DROP_ITEM, DROP_ALL_ITEMS, START_DESTROY_BLOCK -> this.ignoreSwingPackets++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.forge.net.handler;
|
||||
|
||||
import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage;
|
||||
import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage.Handler;
|
||||
import net.minecraftforge.network.simple.SimpleChannel;
|
||||
|
||||
public final class InternalPacketHandler {
|
||||
private static final int PROTOCOL_VERSION = 1;
|
||||
private static final SimpleChannel HANDLER = PacketHandlerUtil
|
||||
.buildLenientHandler("internal", PROTOCOL_VERSION)
|
||||
.simpleChannel();
|
||||
|
||||
private InternalPacketHandler() {
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
HANDLER.registerMessage(0, LeftClickAirEventMessage.class,
|
||||
LeftClickAirEventMessage::encode, LeftClickAirEventMessage::decode, Handler::handle);
|
||||
}
|
||||
|
||||
public static SimpleChannel getHandler() {
|
||||
return HANDLER;
|
||||
}
|
||||
}
|
@ -21,28 +21,27 @@
|
||||
|
||||
import com.sk89q.worldedit.forge.ForgeWorldEdit;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.network.NetworkRegistry;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraftforge.network.Channel;
|
||||
import net.minecraftforge.network.ChannelBuilder;
|
||||
|
||||
final class PacketHandlerUtil {
|
||||
private PacketHandlerUtil() {
|
||||
}
|
||||
|
||||
static NetworkRegistry.ChannelBuilder buildLenientHandler(String id, int protocolVersion) {
|
||||
final String verStr = Integer.toString(protocolVersion);
|
||||
final Predicate<String> validator = validateLenient(verStr);
|
||||
return NetworkRegistry.ChannelBuilder
|
||||
static ChannelBuilder buildLenientHandler(String id, int protocolVersion) {
|
||||
final Channel.VersionTest validator = validateLenient(protocolVersion);
|
||||
return ChannelBuilder
|
||||
.named(new ResourceLocation(ForgeWorldEdit.MOD_ID, id))
|
||||
.clientAcceptedVersions(validator)
|
||||
.serverAcceptedVersions(validator)
|
||||
.networkProtocolVersion(() -> verStr);
|
||||
.networkProtocolVersion(protocolVersion);
|
||||
}
|
||||
|
||||
private static Predicate<String> validateLenient(String protocolVersion) {
|
||||
return remoteVersion ->
|
||||
protocolVersion.equals(remoteVersion)
|
||||
|| NetworkRegistry.ABSENT.version().equals(remoteVersion)
|
||||
|| NetworkRegistry.ACCEPTVANILLA.equals(remoteVersion);
|
||||
private static Channel.VersionTest validateLenient(int protocolVersion) {
|
||||
return (status, remoteVersion) ->
|
||||
protocolVersion == remoteVersion
|
||||
// These two ignore protocolVersion anyway so it doesn't matter what it is
|
||||
|| Channel.VersionTest.ACCEPT_MISSING.accepts(status, protocolVersion)
|
||||
|| Channel.VersionTest.ACCEPT_VANILLA.accepts(status, protocolVersion);
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,11 @@
|
||||
import com.sk89q.worldedit.LocalSession;
|
||||
import com.sk89q.worldedit.forge.ForgePlayer;
|
||||
import com.sk89q.worldedit.forge.ForgeWorldEdit;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
import net.minecraftforge.network.event.EventNetworkChannel;
|
||||
import net.minecraftforge.event.network.CustomPayloadEvent;
|
||||
import net.minecraftforge.network.EventNetworkChannel;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@ -43,12 +45,16 @@ public static void init() {
|
||||
HANDLER.addListener(WECUIPacketHandler::onPacketData);
|
||||
}
|
||||
|
||||
public static void onPacketData(NetworkEvent.ClientCustomPayloadEvent event) {
|
||||
ServerPlayer player = event.getSource().get().getSender();
|
||||
public static void onPacketData(CustomPayloadEvent event) {
|
||||
ServerPlayer player = event.getSource().getSender();
|
||||
LocalSession session = ForgeWorldEdit.inst.getSession(player);
|
||||
String text = event.getPayload().toString(StandardCharsets.UTF_8);
|
||||
final ForgePlayer actor = adaptPlayer(player);
|
||||
session.handleCUIInitializationMessage(text, actor);
|
||||
}
|
||||
|
||||
public static void send(Connection connection, FriendlyByteBuf friendlyByteBuf) {
|
||||
HANDLER.send(friendlyByteBuf, connection);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* WorldEdit, a Minecraft world manipulation toolkit
|
||||
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||
* Copyright (C) WorldEdit team and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.forge.net.packet;
|
||||
|
||||
import com.sk89q.worldedit.forge.ForgeWorldEdit;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent.LeftClickEmpty;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class LeftClickAirEventMessage {
|
||||
|
||||
public static final LeftClickAirEventMessage INSTANCE = new LeftClickAirEventMessage();
|
||||
|
||||
public static final class Handler {
|
||||
public static void handle(final LeftClickAirEventMessage message, Supplier<NetworkEvent.Context> ctx) {
|
||||
NetworkEvent.Context context = ctx.get();
|
||||
context.enqueueWork(() -> ForgeWorldEdit.inst.onPlayerInteract(new LeftClickEmpty(Objects.requireNonNull(context.getSender()))));
|
||||
}
|
||||
}
|
||||
|
||||
public static LeftClickAirEventMessage decode(FriendlyByteBuf buf) {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static void encode(LeftClickAirEventMessage msg, FriendlyByteBuf buf) {
|
||||
}
|
||||
|
||||
private LeftClickAirEventMessage() {
|
||||
}
|
||||
|
||||
}
|
@ -7,3 +7,5 @@ public net.minecraft.server.level.ServerChunkCache m_8456_(IILnet/minecraft/worl
|
||||
|
||||
public net.minecraft.world.level.chunk.ChunkBiomeContainer f_62112_ # biomes
|
||||
public-f net.minecraft.world.level.storage.PrimaryLevelData f_244409_ # worldOptions
|
||||
|
||||
public net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket <init>(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/nbt/CompoundTag;)V # constructor
|
||||
|
@ -3,7 +3,9 @@
|
||||
"package": "com.sk89q.worldedit.forge.mixin",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"mixins": [
|
||||
"MixinLevelChunkSetBlockHook"
|
||||
"AccessorServerPlayerGameMode",
|
||||
"MixinLevelChunkSetBlockHook",
|
||||
"MixinServerGamePacketListenerImpl"
|
||||
],
|
||||
"server": [
|
||||
],
|
||||
|
@ -188,6 +188,11 @@ public Set<SideEffect> getSupportedSideEffects() {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTickCount() {
|
||||
return Sponge.server().runningTimeTicks().ticks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Actor> getConnectedUsers() {
|
||||
return Sponge.server().onlinePlayers().stream().map(SpongePlayer::new).collect(toList());
|
||||
|
@ -35,6 +35,7 @@
|
||||
import com.sk89q.worldedit.extension.platform.PlatformManager;
|
||||
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
|
||||
import com.sk89q.worldedit.internal.command.CommandUtil;
|
||||
import com.sk89q.worldedit.internal.event.InteractionDebouncer;
|
||||
import com.sk89q.worldedit.sponge.config.SpongeConfiguration;
|
||||
import com.sk89q.worldedit.world.biome.BiomeCategory;
|
||||
import com.sk89q.worldedit.world.biome.BiomeType;
|
||||
@ -48,7 +49,6 @@
|
||||
import org.spongepowered.api.Sponge;
|
||||
import org.spongepowered.api.block.BlockSnapshot;
|
||||
import org.spongepowered.api.block.BlockType;
|
||||
import org.spongepowered.api.block.BlockTypes;
|
||||
import org.spongepowered.api.block.entity.BlockEntity;
|
||||
import org.spongepowered.api.block.entity.CommandBlock;
|
||||
import org.spongepowered.api.command.Command;
|
||||
@ -57,8 +57,11 @@
|
||||
import org.spongepowered.api.command.CommandResult;
|
||||
import org.spongepowered.api.command.parameter.ArgumentReader;
|
||||
import org.spongepowered.api.config.ConfigDir;
|
||||
import org.spongepowered.api.data.type.HandTypes;
|
||||
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
|
||||
import org.spongepowered.api.event.EventContextKeys;
|
||||
import org.spongepowered.api.event.Listener;
|
||||
import org.spongepowered.api.event.action.InteractEvent;
|
||||
import org.spongepowered.api.event.block.InteractBlockEvent;
|
||||
import org.spongepowered.api.event.filter.cause.Root;
|
||||
import org.spongepowered.api.event.item.inventory.InteractItemEvent;
|
||||
@ -109,6 +112,7 @@ public static SpongeWorldEdit inst() {
|
||||
private final SpongeConfiguration config;
|
||||
private final Path workingDir;
|
||||
|
||||
private InteractionDebouncer debouncer;
|
||||
private SpongePermissionsProvider provider;
|
||||
private SpongePlatform platform;
|
||||
|
||||
@ -130,6 +134,8 @@ public SpongeWorldEdit(Logger logger,
|
||||
@Listener
|
||||
public void onPluginConstruction(ConstructPluginEvent event) {
|
||||
this.platform = new SpongePlatform(this);
|
||||
debouncer = new InteractionDebouncer(platform);
|
||||
|
||||
WorldEdit.getInstance().getPlatformManager().register(platform);
|
||||
|
||||
this.provider = new SpongePermissionsProvider();
|
||||
@ -287,84 +293,117 @@ public List<CommandCompletion> complete(CommandCause cause, ArgumentReader.Mutab
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isHookingEvents() {
|
||||
return platform != null && platform.isHookingEvents();
|
||||
private boolean skipEvents() {
|
||||
return platform == null || !platform.isHookingEvents();
|
||||
}
|
||||
|
||||
private boolean skipInteractionEvent(InteractEvent event) {
|
||||
return skipEvents() || event.context().get(EventContextKeys.USED_HAND).orElse(null) != HandTypes.MAIN_HAND.get();
|
||||
}
|
||||
|
||||
@Listener
|
||||
public void onPlayerItemInteract(InteractItemEvent.Secondary event, @Root ServerPlayer spongePlayer) {
|
||||
if (!isHookingEvents()) {
|
||||
public void onPlayerInteractItemPrimary(InteractItemEvent.Primary event, @Root ServerPlayer spongePlayer) {
|
||||
if (skipInteractionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
|
||||
SpongePlayer player = SpongeAdapter.adapt(spongePlayer);
|
||||
if (we.handleRightClick(player)) {
|
||||
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean result = we.handleArmSwing(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
}
|
||||
|
||||
@Listener
|
||||
public void onPlayerInteractItemSecondary(InteractItemEvent.Secondary event, @Root ServerPlayer spongePlayer) {
|
||||
if (skipInteractionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
SpongePlayer player = SpongeAdapter.adapt(spongePlayer);
|
||||
|
||||
Optional<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
|
||||
if (previousResult.isPresent()) {
|
||||
if (previousResult.get()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
boolean result = we.handleRightClick(player);
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (result) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Listener
|
||||
public void onPlayerInteract(InteractBlockEvent event, @Root ServerPlayer spongePlayer) {
|
||||
if (platform == null) {
|
||||
public void onPlayerInteractBlockPrimary(InteractBlockEvent.Primary.Start event, @Root ServerPlayer spongePlayer) {
|
||||
if (skipInteractionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!platform.isHookingEvents()) {
|
||||
return; // We have to be told to catch these events
|
||||
}
|
||||
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
|
||||
SpongePlayer player = SpongeAdapter.adapt(spongePlayer);
|
||||
|
||||
BlockSnapshot targetBlock = event.block();
|
||||
Optional<ServerLocation> optLoc = targetBlock.location();
|
||||
|
||||
BlockType interactedType = targetBlock.state().type();
|
||||
if (event instanceof InteractBlockEvent.Primary.Start) {
|
||||
InteractBlockEvent.Primary.Start eventCast = ((InteractBlockEvent.Primary.Start) event);
|
||||
if (interactedType != BlockTypes.AIR.get()) {
|
||||
if (!optLoc.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServerLocation loc = optLoc.get();
|
||||
com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(
|
||||
loc, Vector3d.ZERO
|
||||
);
|
||||
|
||||
if (we.handleBlockLeftClick(player, pos, SpongeAdapter.adapt(eventCast.targetSide()))) {
|
||||
eventCast.setCancelled(true);
|
||||
}
|
||||
}
|
||||
if (we.handleArmSwing(player)) {
|
||||
eventCast.setCancelled(true);
|
||||
}
|
||||
} else if (event instanceof InteractBlockEvent.Secondary) {
|
||||
if (!optLoc.isPresent()) {
|
||||
return;
|
||||
}
|
||||
InteractBlockEvent.Secondary eventCast = ((InteractBlockEvent.Secondary) event);
|
||||
|
||||
boolean result = false;
|
||||
if (optLoc.isPresent()) {
|
||||
ServerLocation loc = optLoc.get();
|
||||
com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(
|
||||
loc, Vector3d.ZERO
|
||||
);
|
||||
com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(loc, Vector3d.ZERO);
|
||||
|
||||
if (we.handleBlockRightClick(player, pos, SpongeAdapter.adapt(eventCast.targetSide()))) {
|
||||
eventCast.setCancelled(true);
|
||||
}
|
||||
result = we.handleBlockLeftClick(player, pos, SpongeAdapter.adapt(event.targetSide()));
|
||||
}
|
||||
|
||||
if (we.handleRightClick(player)) {
|
||||
eventCast.setCancelled(true);
|
||||
}
|
||||
result = we.handleArmSwing(player) || result;
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (result) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Listener
|
||||
public void onPlayerInteractBlockSecondary(InteractBlockEvent.Secondary event, @Root ServerPlayer spongePlayer) {
|
||||
if (skipInteractionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEdit we = WorldEdit.getInstance();
|
||||
SpongePlayer player = SpongeAdapter.adapt(spongePlayer);
|
||||
|
||||
BlockSnapshot targetBlock = event.block();
|
||||
Optional<ServerLocation> optLoc = targetBlock.location();
|
||||
|
||||
boolean result = false;
|
||||
if (optLoc.isPresent()) {
|
||||
ServerLocation loc = optLoc.get();
|
||||
com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(loc, Vector3d.ZERO);
|
||||
|
||||
result = we.handleBlockRightClick(player, pos, SpongeAdapter.adapt(event.targetSide()));
|
||||
}
|
||||
|
||||
result = we.handleRightClick(player) || result;
|
||||
debouncer.setLastInteraction(player, result);
|
||||
|
||||
if (result) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Listener
|
||||
public void onPlayerQuit(ServerSideConnectionEvent.Disconnect event) {
|
||||
debouncer.clearInteraction(SpongeAdapter.adapt(event.player()));
|
||||
|
||||
WorldEdit.getInstance().getEventBus()
|
||||
.post(new SessionIdleEvent(new SpongePlayer.SessionKeyImpl(event.player())));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user