mirror of
https://github.com/boy0001/FastAsyncWorldedit.git
synced 2024-11-25 03:55:35 +01:00
Various
Optimize spline Translate spline brush Add various new patterns (#nox #noy #noz #rel `[stone,wood,blah` #existing Can now use percentages with patterns, not just blocks e.g. 50%#clipboard,50%stone Add resettable patterns
This commit is contained in:
parent
5b96a52e99
commit
1e79ae4a0f
@ -41,6 +41,7 @@ import com.sk89q.worldedit.command.tool.RecursivePickaxe;
|
|||||||
import com.sk89q.worldedit.command.tool.brush.GravityBrush;
|
import com.sk89q.worldedit.command.tool.brush.GravityBrush;
|
||||||
import com.sk89q.worldedit.event.extent.EditSessionEvent;
|
import com.sk89q.worldedit.event.extent.EditSessionEvent;
|
||||||
import com.sk89q.worldedit.extension.factory.DefaultMaskParser;
|
import com.sk89q.worldedit.extension.factory.DefaultMaskParser;
|
||||||
|
import com.sk89q.worldedit.extension.factory.HashTagPatternParser;
|
||||||
import com.sk89q.worldedit.extension.platform.CommandManager;
|
import com.sk89q.worldedit.extension.platform.CommandManager;
|
||||||
import com.sk89q.worldedit.extension.platform.PlatformManager;
|
import com.sk89q.worldedit.extension.platform.PlatformManager;
|
||||||
import com.sk89q.worldedit.extent.AbstractDelegateExtent;
|
import com.sk89q.worldedit.extent.AbstractDelegateExtent;
|
||||||
@ -71,6 +72,7 @@ import com.sk89q.worldedit.function.visitor.RecursiveVisitor;
|
|||||||
import com.sk89q.worldedit.function.visitor.RegionVisitor;
|
import com.sk89q.worldedit.function.visitor.RegionVisitor;
|
||||||
import com.sk89q.worldedit.history.change.EntityCreate;
|
import com.sk89q.worldedit.history.change.EntityCreate;
|
||||||
import com.sk89q.worldedit.history.change.EntityRemove;
|
import com.sk89q.worldedit.history.change.EntityRemove;
|
||||||
|
import com.sk89q.worldedit.math.interpolation.KochanekBartelsInterpolation;
|
||||||
import com.sk89q.worldedit.regions.CuboidRegion;
|
import com.sk89q.worldedit.regions.CuboidRegion;
|
||||||
import com.sk89q.worldedit.regions.selector.CuboidRegionSelector;
|
import com.sk89q.worldedit.regions.selector.CuboidRegionSelector;
|
||||||
import com.sk89q.worldedit.session.SessionManager;
|
import com.sk89q.worldedit.session.SessionManager;
|
||||||
@ -386,6 +388,7 @@ public class Fawe {
|
|||||||
Patterns.inject(); // Optimizations (reduce object creation)
|
Patterns.inject(); // Optimizations (reduce object creation)
|
||||||
RandomPattern.inject(); // Optimizations
|
RandomPattern.inject(); // Optimizations
|
||||||
ClipboardPattern.inject(); // Optimizations
|
ClipboardPattern.inject(); // Optimizations
|
||||||
|
HashTagPatternParser.inject(); // Add new patterns
|
||||||
// Mask
|
// Mask
|
||||||
BlockMask.inject(); // Optimizations
|
BlockMask.inject(); // Optimizations
|
||||||
SolidBlockMask.inject(); // Optimizations
|
SolidBlockMask.inject(); // Optimizations
|
||||||
@ -403,6 +406,8 @@ public class Fawe {
|
|||||||
// NBT
|
// NBT
|
||||||
NBTInputStream.inject(); // Add actual streaming + Optimizations + New methods
|
NBTInputStream.inject(); // Add actual streaming + Optimizations + New methods
|
||||||
NBTOutputStream.inject(); // New methods
|
NBTOutputStream.inject(); // New methods
|
||||||
|
// Math
|
||||||
|
KochanekBartelsInterpolation.inject(); // Optimizations
|
||||||
try {
|
try {
|
||||||
CommandManager.inject(); // Async commands
|
CommandManager.inject(); // Async commands
|
||||||
PlatformManager.inject(); // Async brushes / tools
|
PlatformManager.inject(); // Async brushes / tools
|
||||||
|
@ -93,7 +93,10 @@ public enum BBC {
|
|||||||
BRUSH_SMOOTH("Smooth brush equipped (%s0 x %s1 using %s2).", "WorldEdit.Brush"),
|
BRUSH_SMOOTH("Smooth brush equipped (%s0 x %s1 using %s2).", "WorldEdit.Brush"),
|
||||||
BRUSH_SPHERE("Sphere brush shape equipped (%s0).", "WorldEdit.Brush"),
|
BRUSH_SPHERE("Sphere brush shape equipped (%s0).", "WorldEdit.Brush"),
|
||||||
BRUSH_LINE("Line brush shape equipped (%s0).", "WorldEdit.Brush"),
|
BRUSH_LINE("Line brush shape equipped (%s0).", "WorldEdit.Brush"),
|
||||||
BRUSH_SPLINE("Line brush shape equipped (%s0). Right click to select points, left click to execute.", "WorldEdit.Brush"),
|
BRUSH_SPLINE("Line brush shape equipped (%s0). Right click an end to add a shape", "WorldEdit.Brush"),
|
||||||
|
BRUSH_SPLINE_PRIMARY("Added position, left click to spline them together!", "WorldEdit.Brush"),
|
||||||
|
BRUSH_SPLINE_SECONDARY_ERROR("Not enough positions set!", "WorldEdit.Brush"),
|
||||||
|
BRUSH_SPLINE_SECONDARY("Created spline", "WorldEdit.Brush"),
|
||||||
BRUSH_BLEND_BALL("Blend ball brush equipped (%s0).", "WorldEdit.Brush"),
|
BRUSH_BLEND_BALL("Blend ball brush equipped (%s0).", "WorldEdit.Brush"),
|
||||||
BRUSH_ERODE("Erode brush equipped (%s0).", "WorldEdit.Brush"),
|
BRUSH_ERODE("Erode brush equipped (%s0).", "WorldEdit.Brush"),
|
||||||
BRUSH_PASTE_NONE("Nothing to paste", "WorldEdit.Brush"),
|
BRUSH_PASTE_NONE("Nothing to paste", "WorldEdit.Brush"),
|
||||||
|
@ -68,12 +68,12 @@ public class SplineBrush implements DoubleActionBrush {
|
|||||||
numSplines = points.size();
|
numSplines = points.size();
|
||||||
}
|
}
|
||||||
this.positionSets.add(points);
|
this.positionSets.add(points);
|
||||||
player.print("Added position, right click to spline them together!");
|
BBC.BRUSH_SPLINE_PRIMARY.send(player);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SECONDARY: {
|
case SECONDARY: {
|
||||||
if (positionSets.size() < 2) {
|
if (positionSets.size() < 2) {
|
||||||
player.print("Not enough positions set!");
|
BBC.BRUSH_SPLINE_SECONDARY_ERROR.send(player);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<Vector> centroids = new ArrayList<>();
|
List<Vector> centroids = new ArrayList<>();
|
||||||
@ -112,7 +112,7 @@ public class SplineBrush implements DoubleActionBrush {
|
|||||||
}
|
}
|
||||||
editSession.drawSpline(Patterns.wrap(pattern), currentSpline, 0, 0, 0, 10, 0, true);
|
editSession.drawSpline(Patterns.wrap(pattern), currentSpline, 0, 0, 0, 10, 0, true);
|
||||||
}
|
}
|
||||||
player.print("Created spline");
|
BBC.BRUSH_SPLINE_SECONDARY.send(player);
|
||||||
positionSets.clear();
|
positionSets.clear();
|
||||||
numSplines = 0;
|
numSplines = 0;
|
||||||
break;
|
break;
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.extent.Extent;
|
||||||
|
import com.sk89q.worldedit.function.pattern.AbstractPattern;
|
||||||
|
|
||||||
|
public class ExistingPattern extends AbstractPattern {
|
||||||
|
private final Extent extent;
|
||||||
|
|
||||||
|
public ExistingPattern(Extent extent) {
|
||||||
|
this.extent = extent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseBlock apply(Vector position) {
|
||||||
|
return extent.getBlock(position);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.function.pattern.AbstractPattern;
|
||||||
|
|
||||||
|
public class LinearBlockPattern extends AbstractPattern {
|
||||||
|
|
||||||
|
private final BaseBlock[] blocks;
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
public LinearBlockPattern(BaseBlock[] blocks) {
|
||||||
|
this.blocks = blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseBlock apply(Vector position) {
|
||||||
|
if (index == blocks.length) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
return blocks[index++];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.function.pattern.AbstractPattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.Pattern;
|
||||||
|
|
||||||
|
public class NoXPattern extends AbstractPattern {
|
||||||
|
|
||||||
|
private final Pattern pattern;
|
||||||
|
|
||||||
|
public NoXPattern(Pattern pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector mutable = new Vector();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseBlock apply(Vector pos) {
|
||||||
|
mutable.y = pos.y;
|
||||||
|
mutable.z = pos.z;
|
||||||
|
return pattern.apply(mutable);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.function.pattern.AbstractPattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.Pattern;
|
||||||
|
|
||||||
|
public class NoYPattern extends AbstractPattern {
|
||||||
|
|
||||||
|
private final Pattern pattern;
|
||||||
|
|
||||||
|
public NoYPattern(Pattern pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector mutable = new Vector();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseBlock apply(Vector pos) {
|
||||||
|
mutable.x = pos.x;
|
||||||
|
mutable.z = pos.z;
|
||||||
|
return pattern.apply(mutable);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.function.pattern.AbstractPattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.Pattern;
|
||||||
|
|
||||||
|
public class NoZPattern extends AbstractPattern {
|
||||||
|
|
||||||
|
private final Pattern pattern;
|
||||||
|
|
||||||
|
public NoZPattern(Pattern pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector mutable = new Vector();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseBlock apply(Vector pos) {
|
||||||
|
mutable.x = pos.x;
|
||||||
|
mutable.y = pos.y;
|
||||||
|
return pattern.apply(mutable);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.extent.Extent;
|
||||||
|
import com.sk89q.worldedit.function.pattern.Pattern;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class PatternTraverser {
|
||||||
|
private final Object pattern;
|
||||||
|
|
||||||
|
public PatternTraverser(Object start) {
|
||||||
|
this.pattern = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(Extent newExtent) {
|
||||||
|
reset(pattern, newExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset(Object pattern, Extent newExtent) {
|
||||||
|
if (pattern == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pattern instanceof ResettablePattern) {
|
||||||
|
((ResettablePattern) pattern).reset();
|
||||||
|
}
|
||||||
|
Class<?> current = pattern.getClass();
|
||||||
|
while(current.getSuperclass() != null) {
|
||||||
|
if (newExtent != null) {
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField("extent");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(pattern, newExtent);
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException ignore) {}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField("pattern");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Pattern next = (Pattern) field.get(pattern);
|
||||||
|
reset(next, newExtent);
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException ignore) {}
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField("material");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Pattern next = (Pattern) field.get(pattern);
|
||||||
|
reset(next, newExtent);
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException ignore) {}
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField("patterns");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Collection<Pattern> patterns = (Collection<Pattern>) field.get(pattern);
|
||||||
|
for (Pattern next : patterns) {
|
||||||
|
reset(next, newExtent);
|
||||||
|
}
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException ignore) {}
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.function.pattern.AbstractPattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.Pattern;
|
||||||
|
|
||||||
|
public class RelativePattern extends AbstractPattern implements ResettablePattern {
|
||||||
|
|
||||||
|
private final Pattern pattern;
|
||||||
|
|
||||||
|
public RelativePattern(Pattern pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector origin;
|
||||||
|
private Vector mutable = new Vector();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseBlock apply(Vector pos) {
|
||||||
|
if (origin == null) {
|
||||||
|
origin = new Vector(pos);
|
||||||
|
}
|
||||||
|
mutable.x = pos.x - origin.x;
|
||||||
|
mutable.y = pos.y - origin.y;
|
||||||
|
mutable.z = pos.z - origin.z;
|
||||||
|
return pattern.apply(mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
origin = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.boydti.fawe.object.pattern;
|
||||||
|
|
||||||
|
public interface ResettablePattern {
|
||||||
|
void reset();
|
||||||
|
}
|
@ -18,6 +18,9 @@ public class MaskTraverser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reset(Mask mask, Extent newExtent) {
|
private void reset(Mask mask, Extent newExtent) {
|
||||||
|
if (mask == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mask instanceof ResettableMask) {
|
if (mask instanceof ResettableMask) {
|
||||||
((ResettableMask) mask).reset();
|
((ResettableMask) mask).reset();
|
||||||
}
|
}
|
||||||
|
@ -2700,14 +2700,20 @@ public class EditSession extends AbstractWorld implements HasFaweQueue {
|
|||||||
final int tipx = (int) Math.round(tipv.getX());
|
final int tipx = (int) Math.round(tipv.getX());
|
||||||
final int tipy = (int) Math.round(tipv.getY());
|
final int tipy = (int) Math.round(tipv.getY());
|
||||||
final int tipz = (int) Math.round(tipv.getZ());
|
final int tipz = (int) Math.round(tipv.getZ());
|
||||||
vset.add(new Vector(tipx, tipy, tipz));
|
if (radius == 0) {
|
||||||
|
setBlock(tipx, tipy, tipz, pattern.next(tipx, tipy, tipz));
|
||||||
|
} else {
|
||||||
|
vset.add(new Vector(tipx, tipy, tipz));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (radius != 0) {
|
||||||
vset = this.getBallooned(vset, radius);
|
vset = this.getBallooned(vset, radius);
|
||||||
if (!filled) {
|
if (!filled) {
|
||||||
vset = this.getHollowed(vset);
|
vset = this.getHollowed(vset);
|
||||||
|
}
|
||||||
|
return this.setBlocks(vset, pattern);
|
||||||
}
|
}
|
||||||
return this.setBlocks(vset, pattern);
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double hypot(final double... pars) {
|
private double hypot(final double... pars) {
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
package com.sk89q.worldedit.extension.factory;
|
||||||
|
|
||||||
|
import com.boydti.fawe.object.pattern.ExistingPattern;
|
||||||
|
import com.boydti.fawe.object.pattern.LinearBlockPattern;
|
||||||
|
import com.boydti.fawe.object.pattern.NoXPattern;
|
||||||
|
import com.boydti.fawe.object.pattern.NoYPattern;
|
||||||
|
import com.boydti.fawe.object.pattern.NoZPattern;
|
||||||
|
import com.boydti.fawe.object.pattern.RelativePattern;
|
||||||
|
import com.sk89q.worldedit.EmptyClipboardException;
|
||||||
|
import com.sk89q.worldedit.LocalSession;
|
||||||
|
import com.sk89q.worldedit.WorldEdit;
|
||||||
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
|
import com.sk89q.worldedit.extension.input.InputParseException;
|
||||||
|
import com.sk89q.worldedit.extension.input.ParserContext;
|
||||||
|
import com.sk89q.worldedit.extent.clipboard.Clipboard;
|
||||||
|
import com.sk89q.worldedit.function.pattern.BlockPattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.ClipboardPattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.Pattern;
|
||||||
|
import com.sk89q.worldedit.function.pattern.RandomPattern;
|
||||||
|
import com.sk89q.worldedit.internal.registry.InputParser;
|
||||||
|
import com.sk89q.worldedit.session.ClipboardHolder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class HashTagPatternParser extends InputParser<Pattern> {
|
||||||
|
|
||||||
|
public HashTagPatternParser(WorldEdit worldEdit) {
|
||||||
|
super(worldEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pattern parseFromInput(String input, ParserContext context) throws InputParseException {
|
||||||
|
switch (input.toLowerCase().charAt(0)) {
|
||||||
|
case '#': {
|
||||||
|
switch (input) {
|
||||||
|
case "#existing": {
|
||||||
|
return new ExistingPattern(context.requireExtent());
|
||||||
|
}
|
||||||
|
case "#clipboard":
|
||||||
|
case "#copy": {
|
||||||
|
LocalSession session = context.requireSession();
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
ClipboardHolder holder = session.getClipboard();
|
||||||
|
Clipboard clipboard = holder.getClipboard();
|
||||||
|
return new ClipboardPattern(clipboard);
|
||||||
|
} catch (EmptyClipboardException e) {
|
||||||
|
throw new InputParseException("To use #clipboard, please first copy something to your clipboard");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InputParseException("No session is available, so no clipboard is available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String[] split2 = input.split(":");
|
||||||
|
if (split2.length > 1) {
|
||||||
|
switch (split2[0]) {
|
||||||
|
case "#relative":
|
||||||
|
case "#rel": {
|
||||||
|
String rest = input.substring(5);
|
||||||
|
return new RelativePattern(parseFromInput(rest, context));
|
||||||
|
}
|
||||||
|
case "#nox": {
|
||||||
|
String rest = input.substring(5);
|
||||||
|
return new NoXPattern(parseFromInput(rest, context));
|
||||||
|
}
|
||||||
|
case "#noy": {
|
||||||
|
String rest = input.substring(5);
|
||||||
|
return new NoYPattern(parseFromInput(rest, context));
|
||||||
|
}
|
||||||
|
case "#noz": {
|
||||||
|
String rest = input.substring(5);
|
||||||
|
return new NoZPattern(parseFromInput(rest, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InputParseException("Invalid, see: https://github.com/boy0001/FastAsyncWorldedit/wiki/WorldEdit-and-FAWE-patterns");
|
||||||
|
}
|
||||||
|
case '[': {
|
||||||
|
ArrayList<BaseBlock> blocks = new ArrayList<>();
|
||||||
|
for (String token : input.substring(1).split(",")) {
|
||||||
|
BlockFactory blockRegistry = worldEdit.getBlockFactory();
|
||||||
|
BaseBlock block = blockRegistry.parseFromInput(token, context);
|
||||||
|
blocks.add(block);
|
||||||
|
}
|
||||||
|
if (blocks.isEmpty()) {
|
||||||
|
throw new InputParseException("No blocks provided for linear pattern e.g. [stone,wood");
|
||||||
|
}
|
||||||
|
return new LinearBlockPattern(blocks.toArray(new BaseBlock[blocks.size()]));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
String[] items = input.split(",");
|
||||||
|
if (items.length == 1) {
|
||||||
|
return new BlockPattern(worldEdit.getBlockFactory().parseFromInput(items[0], context));
|
||||||
|
}
|
||||||
|
BlockFactory blockRegistry = worldEdit.getBlockFactory();
|
||||||
|
RandomPattern randomPattern = new RandomPattern();
|
||||||
|
for (String token : input.split(",")) {
|
||||||
|
Pattern pattern;
|
||||||
|
double chance;
|
||||||
|
// Parse special percentage syntax
|
||||||
|
if (token.matches("[0-9]+(\\.[0-9]*)?%.*")) {
|
||||||
|
String[] p = token.split("%");
|
||||||
|
if (p.length < 2) {
|
||||||
|
throw new InputParseException("Missing the pattern after the % symbol for '" + input + "'");
|
||||||
|
} else {
|
||||||
|
chance = Double.parseDouble(p[0]);
|
||||||
|
pattern = parseFromInput(p[1], context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chance = 1;
|
||||||
|
pattern = parseFromInput(token, context);
|
||||||
|
}
|
||||||
|
randomPattern.add(pattern, chance);
|
||||||
|
}
|
||||||
|
return randomPattern;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Class<?> inject() {
|
||||||
|
return HashTagPatternParser.class;
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ package com.sk89q.worldedit.extension.platform;
|
|||||||
import com.boydti.fawe.config.BBC;
|
import com.boydti.fawe.config.BBC;
|
||||||
import com.boydti.fawe.object.FawePlayer;
|
import com.boydti.fawe.object.FawePlayer;
|
||||||
import com.boydti.fawe.object.exception.FaweException;
|
import com.boydti.fawe.object.exception.FaweException;
|
||||||
|
import com.boydti.fawe.object.pattern.PatternTraverser;
|
||||||
import com.boydti.fawe.util.MainUtil;
|
import com.boydti.fawe.util.MainUtil;
|
||||||
import com.boydti.fawe.wrappers.PlayerWrapper;
|
import com.boydti.fawe.wrappers.PlayerWrapper;
|
||||||
import com.sk89q.worldedit.LocalConfiguration;
|
import com.sk89q.worldedit.LocalConfiguration;
|
||||||
@ -330,6 +331,11 @@ public class PlatformManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <T extends Tool> T reset(T tool) {
|
||||||
|
new PatternTraverser(tool).reset(null);
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void handleBlockInteract(BlockInteractEvent event) {
|
public void handleBlockInteract(BlockInteractEvent event) {
|
||||||
@ -371,7 +377,7 @@ public class PlatformManager {
|
|||||||
fp.runAction(new Runnable() {
|
fp.runAction(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
superPickaxe.actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
reset(superPickaxe).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
||||||
}
|
}
|
||||||
}, true, true);
|
}, true, true);
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@ -385,7 +391,7 @@ public class PlatformManager {
|
|||||||
fp.runAction(new Runnable() {
|
fp.runAction(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
((DoubleActionBlockTool) tool).actSecondary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
reset(((DoubleActionBlockTool) tool)).actSecondary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
||||||
}
|
}
|
||||||
}, true, true);
|
}, true, true);
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@ -417,7 +423,7 @@ public class PlatformManager {
|
|||||||
fp.runAction(new Runnable() {
|
fp.runAction(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
((BlockTool) tool).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
reset((BlockTool) tool).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
||||||
}
|
}
|
||||||
}, true, true);
|
}, true, true);
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@ -443,6 +449,7 @@ public class PlatformManager {
|
|||||||
// Create a proxy actor with a potentially different world for
|
// Create a proxy actor with a potentially different world for
|
||||||
// making changes to the world
|
// making changes to the world
|
||||||
final Player player = PlayerWrapper.wrap(createProxyActor(event.getPlayer()));
|
final Player player = PlayerWrapper.wrap(createProxyActor(event.getPlayer()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (event.getInputType()) {
|
switch (event.getInputType()) {
|
||||||
case PRIMARY: {
|
case PRIMARY: {
|
||||||
@ -475,7 +482,7 @@ public class PlatformManager {
|
|||||||
fp.runAsyncIfFree(new Runnable() {
|
fp.runAsyncIfFree(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
((DoubleActionTraceTool) tool).actSecondary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session);
|
reset((DoubleActionTraceTool) tool).actSecondary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@ -513,7 +520,7 @@ public class PlatformManager {
|
|||||||
fp.runAsyncIfFree(new Runnable() {
|
fp.runAsyncIfFree(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
((TraceTool) tool).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session);
|
reset((TraceTool) tool).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
@ -12,7 +12,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
public class ClipboardPattern extends AbstractPattern {
|
public class ClipboardPattern extends AbstractPattern {
|
||||||
|
|
||||||
private final Clipboard clipboard;
|
private final Clipboard clipboard;
|
||||||
private final Vector size;
|
private final int sx, sy, sz;
|
||||||
private final Vector min;
|
private final Vector min;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +23,10 @@ public class ClipboardPattern extends AbstractPattern {
|
|||||||
public ClipboardPattern(Clipboard clipboard) {
|
public ClipboardPattern(Clipboard clipboard) {
|
||||||
checkNotNull(clipboard);
|
checkNotNull(clipboard);
|
||||||
this.clipboard = clipboard;
|
this.clipboard = clipboard;
|
||||||
this.size = clipboard.getMaximumPoint().subtract(clipboard.getMinimumPoint()).add(1, 1, 1);
|
Vector size = clipboard.getMaximumPoint().subtract(clipboard.getMinimumPoint()).add(1, 1, 1);
|
||||||
|
this.sx = size.getBlockX();
|
||||||
|
this.sy = size.getBlockY();
|
||||||
|
this.sz = size.getBlockZ();
|
||||||
this.min = clipboard.getMinimumPoint();
|
this.min = clipboard.getMinimumPoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,9 +34,12 @@ public class ClipboardPattern extends AbstractPattern {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BaseBlock apply(Vector position) {
|
public BaseBlock apply(Vector position) {
|
||||||
int xp = Math.abs(position.getBlockX()) % size.getBlockX();
|
int xp = position.getBlockX() % sx;
|
||||||
int yp = Math.abs(position.getBlockY()) % size.getBlockY();
|
int yp = position.getBlockY() % sy;
|
||||||
int zp = Math.abs(position.getBlockZ()) % size.getBlockZ();
|
int zp = position.getBlockZ() % sz;
|
||||||
|
if (xp < 0) xp += sx;
|
||||||
|
if (yp < 0) yp += sy;
|
||||||
|
if (zp < 0) zp += sz;
|
||||||
mutable.x = min.x + xp;
|
mutable.x = min.x + xp;
|
||||||
mutable.y = min.y + yp;
|
mutable.y = min.y + yp;
|
||||||
mutable.z = min.z + zp;
|
mutable.z = min.z + zp;
|
||||||
|
@ -5,6 +5,7 @@ import com.sk89q.worldedit.Vector;
|
|||||||
import com.sk89q.worldedit.blocks.BaseBlock;
|
import com.sk89q.worldedit.blocks.BaseBlock;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
@ -16,6 +17,7 @@ public class RandomPattern extends AbstractPattern {
|
|||||||
|
|
||||||
private Map<Pattern, Double> weights = new HashMap<>();
|
private Map<Pattern, Double> weights = new HashMap<>();
|
||||||
private RandomCollection<Pattern> collection;
|
private RandomCollection<Pattern> collection;
|
||||||
|
private Set<Pattern> patterns;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a pattern to the weight list of patterns.
|
* Add a pattern to the weight list of patterns.
|
||||||
@ -30,6 +32,7 @@ public class RandomPattern extends AbstractPattern {
|
|||||||
checkNotNull(pattern);
|
checkNotNull(pattern);
|
||||||
weights.put(pattern, chance);
|
weights.put(pattern, chance);
|
||||||
collection = RandomCollection.of(weights);
|
collection = RandomCollection.of(weights);
|
||||||
|
this.patterns = weights.keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.math.interpolation;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.Vector;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Kochanek-Bartels interpolation; continuous in the 2nd derivative.
|
||||||
|
*
|
||||||
|
* <p>Supports {@link Node#tension tension}, {@link Node#bias bias} and
|
||||||
|
* {@link Node#continuity continuity} parameters per {@link Node}.</p>
|
||||||
|
*/
|
||||||
|
public class KochanekBartelsInterpolation implements Interpolation {
|
||||||
|
|
||||||
|
private List<Node> nodes;
|
||||||
|
private Vector[] coeffA;
|
||||||
|
private Vector[] coeffB;
|
||||||
|
private Vector[] coeffC;
|
||||||
|
private Vector[] coeffD;
|
||||||
|
private double scaling;
|
||||||
|
|
||||||
|
public KochanekBartelsInterpolation() {
|
||||||
|
setNodes(Collections.<Node>emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNodes(List<Node> nodes) {
|
||||||
|
checkNotNull(nodes);
|
||||||
|
|
||||||
|
this.nodes = nodes;
|
||||||
|
recalc();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recalc() {
|
||||||
|
final int nNodes = nodes.size();
|
||||||
|
coeffA = new Vector[nNodes];
|
||||||
|
coeffB = new Vector[nNodes];
|
||||||
|
coeffC = new Vector[nNodes];
|
||||||
|
coeffD = new Vector[nNodes];
|
||||||
|
|
||||||
|
if (nNodes == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Node nodeB = nodes.get(0);
|
||||||
|
double tensionB = nodeB.getTension();
|
||||||
|
double biasB = nodeB.getBias();
|
||||||
|
double continuityB = nodeB.getContinuity();
|
||||||
|
for (int i = 0; i < nNodes; ++i) {
|
||||||
|
final double tensionA = tensionB;
|
||||||
|
final double biasA = biasB;
|
||||||
|
final double continuityA = continuityB;
|
||||||
|
|
||||||
|
if (i + 1 < nNodes) {
|
||||||
|
nodeB = nodes.get(i + 1);
|
||||||
|
tensionB = nodeB.getTension();
|
||||||
|
biasB = nodeB.getBias();
|
||||||
|
continuityB = nodeB.getContinuity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kochanek-Bartels tangent coefficients
|
||||||
|
final double ta = (1-tensionA)*(1+biasA)*(1+continuityA)/2; // Factor for lhs of d[i]
|
||||||
|
final double tb = (1-tensionA)*(1-biasA)*(1-continuityA)/2; // Factor for rhs of d[i]
|
||||||
|
final double tc = (1-tensionB)*(1+biasB)*(1-continuityB)/2; // Factor for lhs of d[i+1]
|
||||||
|
final double td = (1-tensionB)*(1-biasB)*(1+continuityB)/2; // Factor for rhs of d[i+1]
|
||||||
|
|
||||||
|
coeffA[i] = linearCombination(i, -ta, ta- tb-tc+2, tb+tc-td-2, td);
|
||||||
|
coeffB[i] = linearCombination(i, 2*ta, -2*ta+2*tb+tc-3, -2*tb-tc+td+3, -td);
|
||||||
|
coeffC[i] = linearCombination(i, -ta, ta- tb , tb , 0);
|
||||||
|
//coeffD[i] = linearCombination(i, 0, 1, 0, 0);
|
||||||
|
coeffD[i] = retrieve(i); // this is an optimization
|
||||||
|
}
|
||||||
|
|
||||||
|
scaling = nodes.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the linear combination of the given coefficients with the nodes adjacent to baseIndex.
|
||||||
|
*
|
||||||
|
* @param baseIndex node index
|
||||||
|
* @param f1 coefficient for baseIndex-1
|
||||||
|
* @param f2 coefficient for baseIndex
|
||||||
|
* @param f3 coefficient for baseIndex+1
|
||||||
|
* @param f4 coefficient for baseIndex+2
|
||||||
|
* @return linear combination of nodes[n-1..n+2] with f1..4
|
||||||
|
*/
|
||||||
|
private Vector linearCombination(int baseIndex, double f1, double f2, double f3, double f4) {
|
||||||
|
final Vector r1 = retrieve(baseIndex - 1).multiply(f1);
|
||||||
|
final Vector r2 = retrieve(baseIndex ).multiply(f2);
|
||||||
|
final Vector r3 = retrieve(baseIndex + 1).multiply(f3);
|
||||||
|
final Vector r4 = retrieve(baseIndex + 2).multiply(f4);
|
||||||
|
|
||||||
|
return r1.add(r2).add(r3).add(r4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a node. Indexes are clamped to the valid range.
|
||||||
|
*
|
||||||
|
* @param index node index to retrieve
|
||||||
|
* @return nodes[clamp(0, nodes.length-1)]
|
||||||
|
*/
|
||||||
|
private Vector retrieve(int index) {
|
||||||
|
if (index < 0)
|
||||||
|
return fastRetrieve(0);
|
||||||
|
|
||||||
|
if (index >= nodes.size())
|
||||||
|
return fastRetrieve(nodes.size()-1);
|
||||||
|
|
||||||
|
return fastRetrieve(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector fastRetrieve(int index) {
|
||||||
|
return nodes.get(index).getPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector mutable = new Vector();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vector getPosition(double position) {
|
||||||
|
if (coeffA == null)
|
||||||
|
throw new IllegalStateException("Must call setNodes first.");
|
||||||
|
|
||||||
|
if (position > 1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
position *= scaling;
|
||||||
|
|
||||||
|
final int index = (int) Math.floor(position);
|
||||||
|
final double remainder = position - index;
|
||||||
|
|
||||||
|
final Vector a = coeffA[index];
|
||||||
|
final Vector b = coeffB[index];
|
||||||
|
final Vector c = coeffC[index];
|
||||||
|
final Vector d = coeffD[index];
|
||||||
|
|
||||||
|
double r2 = remainder * remainder;
|
||||||
|
double r3 = r2 * remainder;
|
||||||
|
mutable.x = a.x * r3 + b.x * r2 + c.x * remainder + d.x;
|
||||||
|
mutable.y = a.y * r3 + b.y * r2 + c.y * remainder + d.y;
|
||||||
|
mutable.z = a.z * r3 + b.z * r2 + c.z * remainder + d.z;
|
||||||
|
return mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vector get1stDerivative(double position) {
|
||||||
|
if (coeffA == null)
|
||||||
|
throw new IllegalStateException("Must call setNodes first.");
|
||||||
|
|
||||||
|
if (position > 1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
position *= scaling;
|
||||||
|
|
||||||
|
final int index = (int) Math.floor(position);
|
||||||
|
//final double remainder = position - index;
|
||||||
|
|
||||||
|
final Vector a = coeffA[index];
|
||||||
|
final Vector b = coeffB[index];
|
||||||
|
final Vector c = coeffC[index];
|
||||||
|
|
||||||
|
return a.multiply(1.5*position - 3.0*index).add(b).multiply(2.0*position).add(a.multiply(1.5*index).subtract(b).multiply(2.0*index)).add(c).multiply(scaling);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double arcLength(double positionA, double positionB) {
|
||||||
|
if (coeffA == null)
|
||||||
|
throw new IllegalStateException("Must call setNodes first.");
|
||||||
|
|
||||||
|
if (positionA > positionB)
|
||||||
|
return arcLength(positionB, positionA);
|
||||||
|
|
||||||
|
positionA *= scaling;
|
||||||
|
positionB *= scaling;
|
||||||
|
|
||||||
|
final int indexA = (int) Math.floor(positionA);
|
||||||
|
final double remainderA = positionA - indexA;
|
||||||
|
|
||||||
|
final int indexB = (int) Math.floor(positionB);
|
||||||
|
final double remainderB = positionB - indexB;
|
||||||
|
|
||||||
|
return arcLengthRecursive(indexA, remainderA, indexB, remainderB);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes a < b
|
||||||
|
*/
|
||||||
|
private double arcLengthRecursive(int indexLeft, double remainderLeft, int indexRight, double remainderRight) {
|
||||||
|
switch (indexRight - indexLeft) {
|
||||||
|
case 0:
|
||||||
|
return arcLengthRecursive(indexLeft, remainderLeft, remainderRight);
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// This case is merely a speed-up for a very common case
|
||||||
|
return
|
||||||
|
arcLengthRecursive(indexLeft, remainderLeft, 1.0) +
|
||||||
|
arcLengthRecursive(indexRight, 0.0, remainderRight);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
arcLengthRecursive(indexLeft, remainderLeft, indexRight - 1, 1.0) +
|
||||||
|
arcLengthRecursive(indexRight, 0.0, remainderRight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double arcLengthRecursive(int index, double remainderLeft, double remainderRight) {
|
||||||
|
final Vector a = coeffA[index].multiply(3.0);
|
||||||
|
final Vector b = coeffB[index].multiply(2.0);
|
||||||
|
final Vector c = coeffC[index];
|
||||||
|
|
||||||
|
final int nPoints = 8;
|
||||||
|
|
||||||
|
double accum = a.multiply(remainderLeft).add(b).multiply(remainderLeft).add(c).length() / 2.0;
|
||||||
|
for (int i = 1; i < nPoints-1; ++i) {
|
||||||
|
double t = ((double) i) / nPoints;
|
||||||
|
t = (remainderRight-remainderLeft)*t + remainderLeft;
|
||||||
|
accum += a.multiply(t).add(b).multiply(t).add(c).length();
|
||||||
|
}
|
||||||
|
|
||||||
|
accum += a.multiply(remainderRight).add(b).multiply(remainderRight).add(c).length() / 2.0;
|
||||||
|
return accum * (remainderRight - remainderLeft) / nPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSegment(double position) {
|
||||||
|
if (coeffA == null)
|
||||||
|
throw new IllegalStateException("Must call setNodes first.");
|
||||||
|
|
||||||
|
if (position > 1)
|
||||||
|
return Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
position *= scaling;
|
||||||
|
|
||||||
|
return (int) Math.floor(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Class<?> inject() {
|
||||||
|
return KochanekBartelsInterpolation.class;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user