geforkt von Mirrors/FastAsyncWorldEdit
Merge branch 'breaking' of https://github.com/IntellectualSites/FastAsyncWorldEdit-1.13 into breaking
Dieser Commit ist enthalten in:
Commit
48bc4015c2
@ -47,12 +47,16 @@ ext {
|
||||
date = git.head().getDate().format("yy.MM.dd")
|
||||
revision = "-${git.head().abbreviatedId}"
|
||||
parents = git.head().parentIds;
|
||||
index = -2116; // Offset to match CI
|
||||
if (project.hasProperty('buildnumber')) {
|
||||
buildNumber = "$buildnumber"
|
||||
} else {
|
||||
index = -2109; // Offset to match CI
|
||||
for (; parents != null && !parents.isEmpty(); index++) {
|
||||
parents = git.getResolve().toCommit(parents.get(0)).getParentIds()
|
||||
}
|
||||
buildNumber = "${index}"
|
||||
}
|
||||
}
|
||||
|
||||
if ( project.hasProperty("lzNoVersion") ) { // gradle build -PlzNoVersion
|
||||
version = "unknown"
|
||||
|
@ -15,9 +15,17 @@ import org.bukkit.plugin.java.JavaPlugin;
|
||||
*/
|
||||
public class VoxelSniper extends JavaPlugin {
|
||||
private static VoxelSniper instance;
|
||||
private SniperManager sniperManager = new SniperManager(this);
|
||||
private final VoxelSniperListener voxelSniperListener = new VoxelSniperListener(this);
|
||||
private SniperManager sniperManager = new SniperManager(this);
|
||||
private VoxelSniperConfiguration voxelSniperConfiguration;
|
||||
private Brushes brushManager = new Brushes();
|
||||
|
||||
/**
|
||||
* @return {@link VoxelSniper}
|
||||
*/
|
||||
public static VoxelSniper getInstance() {
|
||||
return VoxelSniper.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link com.thevoxelbox.voxelsniper.Brushes} for current instance.
|
||||
@ -28,15 +36,6 @@ public class VoxelSniper extends JavaPlugin {
|
||||
return brushManager;
|
||||
}
|
||||
|
||||
private Brushes brushManager = new Brushes();
|
||||
|
||||
/**
|
||||
* @return {@link VoxelSniper}
|
||||
*/
|
||||
public static VoxelSniper getInstance() {
|
||||
return VoxelSniper.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object for accessing global VoxelSniper options.
|
||||
*
|
||||
@ -67,7 +66,7 @@ public class VoxelSniper extends JavaPlugin {
|
||||
return voxelSniperListener.onCommand((Player) sender, arguments, command.getName());
|
||||
}
|
||||
|
||||
getLogger().info("Only Players can execute commands.");
|
||||
getLogger().info("Only players can execute VoxelSniper commands.");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.thevoxelbox.voxelsniper.command;
|
||||
|
||||
import com.boydti.fawe.config.BBC;
|
||||
import com.thevoxelbox.voxelsniper.Sniper;
|
||||
import com.thevoxelbox.voxelsniper.VoxelSniper;
|
||||
import com.thevoxelbox.voxelsniper.api.command.VoxelCommand;
|
||||
@ -21,12 +22,12 @@ public class VoxelUndoCommand extends VoxelCommand {
|
||||
int amount = Integer.parseInt(args[0]);
|
||||
sniper.undo(amount);
|
||||
} catch (NumberFormatException exception) {
|
||||
player.sendMessage("Error while parsing amount of undo. Number format exception.");
|
||||
player.sendMessage(BBC.getPrefix() + "Number expected; string given.");
|
||||
}
|
||||
} else {
|
||||
sniper.undo();
|
||||
}
|
||||
plugin.getLogger().info("Player \"" + player.getName() + "\" used /u");
|
||||
// plugin.getLogger().info("Player \"" + player.getName() + "\" used /u");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ import org.bukkit.World;
|
||||
/**
|
||||
* @author MikeMatrix
|
||||
*/
|
||||
public class BlockWrapper
|
||||
{
|
||||
public class BlockWrapper {
|
||||
|
||||
private int id;
|
||||
private Material type;
|
||||
@ -22,8 +21,7 @@ public class BlockWrapper
|
||||
* @param block
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public BlockWrapper(final AsyncBlock block)
|
||||
{
|
||||
public BlockWrapper(final AsyncBlock block) {
|
||||
this.setId(block.getTypeId());
|
||||
this.setX(block.getX());
|
||||
this.setY(block.getY());
|
||||
@ -35,111 +33,93 @@ public class BlockWrapper
|
||||
/**
|
||||
* @return the data
|
||||
*/
|
||||
public final int getPropertyId()
|
||||
{
|
||||
public final int getPropertyId() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data the data to set
|
||||
*/
|
||||
public final void setPropertyId(final int data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Material getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(Material type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the id
|
||||
*/
|
||||
public final int getId()
|
||||
{
|
||||
public final int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id the id to set
|
||||
*/
|
||||
public final void setId(final int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the world
|
||||
*/
|
||||
public final World getWorld()
|
||||
{
|
||||
public final World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param world the world to set
|
||||
*/
|
||||
public final void setWorld(final World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the x
|
||||
*/
|
||||
public final int getX()
|
||||
{
|
||||
public final int getX() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x the x to set
|
||||
*/
|
||||
public final void setX(final int x) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the y
|
||||
*/
|
||||
public final int getY()
|
||||
{
|
||||
public final int getY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param y the y to set
|
||||
*/
|
||||
public final void setY(final int y) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the z
|
||||
*/
|
||||
public final int getZ()
|
||||
{
|
||||
public final int getZ() {
|
||||
return this.z;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data
|
||||
* the data to set
|
||||
* @param z the z to set
|
||||
*/
|
||||
public final void setPropertyId(final int data)
|
||||
{
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* the id to set
|
||||
*/
|
||||
public final void setId(final int id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param world
|
||||
* the world to set
|
||||
*/
|
||||
public final void setWorld(final World world)
|
||||
{
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x
|
||||
* the x to set
|
||||
*/
|
||||
public final void setX(final int x)
|
||||
{
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param y
|
||||
* the y to set
|
||||
*/
|
||||
public final void setY(final int y)
|
||||
{
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param z
|
||||
* the z to set
|
||||
*/
|
||||
public final void setZ(final int z)
|
||||
{
|
||||
public final void setZ(final int z) {
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public void setType(Material type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
package com.thevoxelbox.voxelsniper.util;
|
||||
|
||||
import com.thevoxelbox.voxelsniper.Undo;
|
||||
@ -10,26 +9,23 @@ import org.bukkit.block.data.BlockData;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class UndoDelegate implements BlockChangeDelegate
|
||||
{
|
||||
public class UndoDelegate implements BlockChangeDelegate {
|
||||
private final World targetWorld;
|
||||
private Undo currentUndo;
|
||||
|
||||
public Undo getUndo()
|
||||
{
|
||||
public UndoDelegate(World targetWorld) {
|
||||
this.targetWorld = targetWorld;
|
||||
this.currentUndo = new Undo();
|
||||
}
|
||||
|
||||
public Undo getUndo() {
|
||||
final Undo pastUndo = currentUndo;
|
||||
currentUndo = new Undo();
|
||||
return pastUndo;
|
||||
}
|
||||
|
||||
public UndoDelegate(World targetWorld)
|
||||
{
|
||||
this.targetWorld = targetWorld;
|
||||
this.currentUndo = new Undo();
|
||||
}
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean setBlock(Block b)
|
||||
{
|
||||
public boolean setBlock(Block b) {
|
||||
this.currentUndo.put(this.targetWorld.getBlockAt(b.getLocation()));
|
||||
this.targetWorld.getBlockAt(b.getLocation()).setBlockData(b.getBlockData());
|
||||
return true;
|
||||
@ -48,14 +44,12 @@ public class UndoDelegate implements BlockChangeDelegate
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight()
|
||||
{
|
||||
public int getHeight() {
|
||||
return this.targetWorld.getMaxHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty(int x, int y, int z)
|
||||
{
|
||||
public boolean isEmpty(int x, int y, int z) {
|
||||
return this.targetWorld.getBlockAt(x, y, z).isEmpty();
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,7 @@ import java.util.List;
|
||||
/**
|
||||
* Container class for multiple ID/Datavalue pairs.
|
||||
*/
|
||||
public class VoxelList
|
||||
{
|
||||
public class VoxelList {
|
||||
|
||||
private BlockMask mask = new BlockMask();
|
||||
|
||||
@ -26,13 +25,11 @@ public class VoxelList
|
||||
*
|
||||
* @param i
|
||||
*/
|
||||
public void add(BlockState i)
|
||||
{
|
||||
public void add(BlockState i) {
|
||||
this.mask = mask.toBuilder().add(i).build(NullExtent.INSTANCE);
|
||||
}
|
||||
|
||||
public void add(BlockMask mask)
|
||||
{
|
||||
public void add(BlockMask mask) {
|
||||
this.mask = (BlockMask) mask.and(mask);
|
||||
}
|
||||
|
||||
@ -41,14 +38,12 @@ public class VoxelList
|
||||
*
|
||||
* @return true if this list contained the specified element
|
||||
*/
|
||||
public boolean removeValue(final BlockState state)
|
||||
{
|
||||
public boolean removeValue(final BlockState state) {
|
||||
this.mask = mask.toBuilder().remove(state).build(NullExtent.INSTANCE);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeValue(final BlockMask state)
|
||||
{
|
||||
public boolean removeValue(final BlockMask state) {
|
||||
this.mask = (BlockMask) mask.and(state.inverse());
|
||||
return true;
|
||||
}
|
||||
@ -57,16 +52,14 @@ public class VoxelList
|
||||
* @param i
|
||||
* @return true if this list contains the specified element
|
||||
*/
|
||||
public boolean contains(final BlockData i)
|
||||
{
|
||||
public boolean contains(final BlockData i) {
|
||||
return mask.test(BukkitAdapter.adapt(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the VoxelList.
|
||||
*/
|
||||
public void clear()
|
||||
{
|
||||
public void clear() {
|
||||
mask = mask.toBuilder().clear().build(NullExtent.INSTANCE);
|
||||
}
|
||||
|
||||
@ -75,8 +68,7 @@ public class VoxelList
|
||||
*
|
||||
* @return true if this list contains no elements
|
||||
*/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
public boolean isEmpty() {
|
||||
return mask.toBuilder().isEmpty();
|
||||
}
|
||||
|
||||
@ -85,8 +77,7 @@ public class VoxelList
|
||||
*
|
||||
* @return defensive copy of the List with pairs
|
||||
*/
|
||||
public String toString()
|
||||
{
|
||||
public String toString() {
|
||||
return mask.toString();
|
||||
}
|
||||
|
||||
|
Binäre Datei nicht angezeigt.
@ -437,7 +437,7 @@ public class ClipboardCommands extends MethodCommands {
|
||||
@Command(
|
||||
aliases = {"/paste"},
|
||||
usage = "",
|
||||
flags = "sao",
|
||||
flags = "saobe",
|
||||
desc = "Paste the clipboard's contents",
|
||||
help =
|
||||
"Pastes the clipboard's contents.\n" +
|
||||
|
@ -1,144 +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 Lesser 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 Lesser General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sk89q.worldedit.command;
|
||||
|
||||
import com.boydti.fawe.config.BBC;
|
||||
import com.sk89q.minecraft.util.commands.Command;
|
||||
import com.sk89q.minecraft.util.commands.CommandContext;
|
||||
import com.sk89q.minecraft.util.commands.CommandPermissions;
|
||||
import com.sk89q.worldedit.*;
|
||||
import com.sk89q.worldedit.extension.input.DisallowedUsageException;
|
||||
import com.sk89q.worldedit.entity.Player;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* General WorldEdit commands.
|
||||
*/
|
||||
public class GeneralCommands {
|
||||
|
||||
private final WorldEdit worldEdit;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param worldEdit reference to WorldEdit
|
||||
*/
|
||||
public GeneralCommands(WorldEdit worldEdit) {
|
||||
checkNotNull(worldEdit);
|
||||
this.worldEdit = worldEdit;
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "/limit" },
|
||||
usage = "[limit]",
|
||||
desc = "Modify block change limit",
|
||||
min = 0,
|
||||
max = 1
|
||||
)
|
||||
@CommandPermissions("worldedit.limit")
|
||||
public void limit(Player player, LocalSession session, CommandContext args) throws WorldEditException {
|
||||
|
||||
LocalConfiguration config = worldEdit.getConfiguration();
|
||||
boolean mayDisable = player.hasPermission("worldedit.limit.unrestricted");
|
||||
|
||||
int limit = args.argsLength() == 0 ? config.defaultChangeLimit : Math.max(-1, args.getInteger(0));
|
||||
if (!mayDisable && config.maxChangeLimit > -1) {
|
||||
if (limit > config.maxChangeLimit) {
|
||||
player.printError(BBC.getPrefix() + "Your maximum allowable limit is " + config.maxChangeLimit + ".");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
session.setBlockChangeLimit(limit);
|
||||
|
||||
if (limit != config.defaultChangeLimit) {
|
||||
player.print(BBC.getPrefix() + "Block change limit set to " + limit + ". (Use //limit to go back to the default.)");
|
||||
} else {
|
||||
player.print(BBC.getPrefix() + "Block change limit set to " + limit + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "/timeout" },
|
||||
usage = "[time]",
|
||||
desc = "Modify evaluation timeout time.",
|
||||
min = 0,
|
||||
max = 1
|
||||
)
|
||||
@CommandPermissions("worldedit.timeout")
|
||||
public void timeout(Player player, LocalSession session, CommandContext args) throws WorldEditException {
|
||||
|
||||
LocalConfiguration config = worldEdit.getConfiguration();
|
||||
boolean mayDisable = player.hasPermission("worldedit.timeout.unrestricted");
|
||||
|
||||
int limit = args.argsLength() == 0 ? config.calculationTimeout : Math.max(-1, args.getInteger(0));
|
||||
if (!mayDisable && config.maxCalculationTimeout > -1) {
|
||||
if (limit > config.maxCalculationTimeout) {
|
||||
player.printError(BBC.getPrefix() + "Your maximum allowable timeout is " + config.maxCalculationTimeout + " ms.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
session.setTimeout(limit);
|
||||
|
||||
if (limit != config.calculationTimeout) {
|
||||
player.print(BBC.getPrefix() + "Timeout time set to " + limit + " ms. (Use //timeout to go back to the default.)");
|
||||
} else {
|
||||
player.print(BBC.getPrefix() + "Timeout time set to " + limit + " ms.");
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "/drawsel" },
|
||||
usage = "[on|off]",
|
||||
desc = "Toggle drawing the current selection",
|
||||
min = 0,
|
||||
max = 1
|
||||
)
|
||||
@CommandPermissions("worldedit.drawsel")
|
||||
public void drawSelection(Player player, LocalSession session, CommandContext args) throws WorldEditException {
|
||||
|
||||
if (!WorldEdit.getInstance().getConfiguration().serverSideCUI) {
|
||||
throw new DisallowedUsageException("This functionality is disabled in the configuration!");
|
||||
}
|
||||
String newState = args.getString(0, null);
|
||||
if (session.shouldUseServerCUI()) {
|
||||
if ("on".equals(newState)) {
|
||||
player.printError(BBC.getPrefix() + "Server CUI already enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
session.setUseServerCUI(false);
|
||||
session.updateServerCUI(player);
|
||||
player.print("Server CUI disabled.");
|
||||
} else {
|
||||
if ("off".equals(newState)) {
|
||||
player.printError(BBC.getPrefix() + "Server CUI already disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
session.setUseServerCUI(true);
|
||||
session.updateServerCUI(player);
|
||||
player.print("Server CUI enabled. This only supports cuboid regions, with a maximum size of 32x32x32.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ import com.sk89q.minecraft.util.commands.Command;
|
||||
import com.sk89q.minecraft.util.commands.CommandContext;
|
||||
import com.sk89q.minecraft.util.commands.CommandPermissions;
|
||||
import com.sk89q.worldedit.*;
|
||||
import com.sk89q.worldedit.extension.input.DisallowedUsageException;
|
||||
import com.sk89q.worldedit.world.block.BaseBlock;
|
||||
import com.sk89q.worldedit.world.block.BlockState;
|
||||
import com.sk89q.worldedit.entity.Player;
|
||||
@ -239,6 +240,71 @@ public class OptionsCommands {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "/timeout" },
|
||||
usage = "[time]",
|
||||
desc = "Modify evaluation timeout time.",
|
||||
min = 0,
|
||||
max = 1
|
||||
)
|
||||
@CommandPermissions("worldedit.timeout")
|
||||
public void timeout(Player player, LocalSession session, CommandContext args) throws WorldEditException {
|
||||
|
||||
LocalConfiguration config = worldEdit.getConfiguration();
|
||||
boolean mayDisable = player.hasPermission("worldedit.timeout.unrestricted");
|
||||
|
||||
int limit = args.argsLength() == 0 ? config.calculationTimeout : Math.max(-1, args.getInteger(0));
|
||||
if (!mayDisable && config.maxCalculationTimeout > -1) {
|
||||
if (limit > config.maxCalculationTimeout) {
|
||||
player.printError(BBC.getPrefix() + "Your maximum allowable timeout is " + config.maxCalculationTimeout + " ms.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
session.setTimeout(limit);
|
||||
|
||||
if (limit != config.calculationTimeout) {
|
||||
player.print(BBC.getPrefix() + "Timeout time set to " + limit + " ms. (Use //timeout to go back to the default.)");
|
||||
} else {
|
||||
player.print(BBC.getPrefix() + "Timeout time set to " + limit + " ms.");
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "/drawsel" },
|
||||
usage = "[on|off]",
|
||||
desc = "Toggle drawing the current selection",
|
||||
min = 0,
|
||||
max = 1
|
||||
)
|
||||
@CommandPermissions("worldedit.drawsel")
|
||||
public void drawSelection(Player player, LocalSession session, CommandContext args) throws WorldEditException {
|
||||
|
||||
if (!WorldEdit.getInstance().getConfiguration().serverSideCUI) {
|
||||
throw new DisallowedUsageException(BBC.getPrefix() + "This functionality is disabled in the configuration!");
|
||||
}
|
||||
String newState = args.getString(0, null);
|
||||
if (session.shouldUseServerCUI()) {
|
||||
if ("on".equals(newState)) {
|
||||
player.printError(BBC.getPrefix() + "Server CUI already enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
session.setUseServerCUI(false);
|
||||
session.updateServerCUI(player);
|
||||
player.print(BBC.getPrefix() + "Server CUI disabled.");
|
||||
} else {
|
||||
if ("off".equals(newState)) {
|
||||
player.printError(BBC.getPrefix() + "Server CUI already disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
session.setUseServerCUI(true);
|
||||
session.updateServerCUI(player);
|
||||
player.print(BBC.getPrefix() + "Server CUI enabled. This only supports cuboid regions, with a maximum size of 32x32x32.");
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = {"/searchitem", "/l", "/search", "searchitem"},
|
||||
usage = "<query>",
|
||||
|
@ -64,7 +64,6 @@ public final class DocumentationPrinter {
|
||||
classes.add(BiomeCommands.class);
|
||||
classes.add(ChunkCommands.class);
|
||||
classes.add(ClipboardCommands.class);
|
||||
classes.add(GeneralCommands.class);
|
||||
classes.add(GenerationCommands.class);
|
||||
classes.add(HistoryCommands.class);
|
||||
classes.add(NavigationCommands.class);
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren