Map madness

This commit is contained in:
Daniel Mills 2021-07-15 03:38:27 -04:00
parent 9e9feb5980
commit df273aca7e
5 changed files with 343 additions and 167 deletions

View File

@ -21,11 +21,13 @@ package com.volmit.iris.manager.command.studio;
import com.volmit.iris.Iris;
import com.volmit.iris.IrisSettings;
import com.volmit.iris.generator.IrisComplex;
import com.volmit.iris.generator.IrisEngine;
import com.volmit.iris.manager.IrisDataManager;
import com.volmit.iris.manager.gui.IrisVision;
import com.volmit.iris.map.MapVision;
import com.volmit.iris.object.IrisDimension;
import com.volmit.iris.scaffold.IrisWorlds;
import com.volmit.iris.scaffold.engine.Engine;
import com.volmit.iris.scaffold.engine.IrisAccess;
import com.volmit.iris.util.FakeEngine;
import com.volmit.iris.util.FakeWorld;
@ -66,58 +68,18 @@ public class CommandIrisStudioMap extends MortarCommand
return true;
}
IrisComplex complex;
Engine fe;
if (args.length > 0) {
String type = "";
long seed = 1337;
for(String i : args)
{
if (i.contains("=")) {
type = i.startsWith("type=") ? i.split("\\Q=\\E")[1] : type;
seed = i.startsWith("seed=") ? Long.valueOf(i.split("\\Q=\\E")[1]) : seed;
} else {
if (type.equals("")) {
type = i;
} else if (seed == 1337) {
seed = Long.valueOf(i);
}
}
}
if (type.equals("")) {
sender.sendMessage("Open this in a studio world or do /iris studio map [pack]");
return true;
}
IrisDimension dim = IrisDataManager.loadAnyDimension(type);
if (dim == null) {
sender.sendMessage("Can't find dimension: " + type);
return true;
}
if (dim.getEnvironment() == null) {
dim.setEnvironment(World.Environment.NORMAL);
}
//Setup the fake world and engine objects so we can get an IrisComplex for the terrain they will
//generate without actually generating any of it
sender.sendMessage("Preparing map...");
FakeWorld world = new FakeWorld(dim.getName(), 0, 256, seed, new File(dim.getName()), dim.getEnvironment());
FakeEngine engine = new FakeEngine(dim, world);
complex = new IrisComplex(engine, true);
} else if (Iris.proj.isProjectOpen()) {
try {
IrisAccess g = Iris.proj.getActiveProject().getActiveProvider();
complex = g.getCompound().getDefaultEngine().getFramework().getComplex();
sender.sendMessage("Opening map for existing studio world!");
} else {
sender.sendMessage("Open this in a studio world or do /iris studio map [pack]");
return true;
IrisVision.launch(g, 0);
sender.sendMessage("Opening Map!");
} catch (Throwable e) {
Iris.reportError(e);
IrisAccess g = IrisWorlds.access(sender.player().getWorld());
IrisVision.launch(g, 0);
sender.sendMessage("Opening Map!");
}
MapVision map = new MapVision(complex);
map.open();
return true;
}

View File

@ -61,7 +61,7 @@ public class IrisVision extends JPanel implements MouseWheelListener {
private final KMap<BlockPosition, BufferedImage> fastpositions = new KMap<>();
private final KSet<BlockPosition> working = new KSet<>();
private final KSet<BlockPosition> workingfast = new KSet<>();
private final ExecutorService e = Executors.newFixedThreadPool(8, r -> {
private final ExecutorService e = Executors.newFixedThreadPool(24, r -> {
tid++;
Thread t = new Thread(r);
t.setName("Iris HD Renderer " + tid);

View File

@ -2,29 +2,19 @@ package com.volmit.iris.map;
import com.volmit.iris.Iris;
import com.volmit.iris.generator.IrisComplex;
import com.volmit.iris.util.J;
import com.volmit.iris.util.KMap;
import com.volmit.iris.util.PrecisionStopwatch;
import com.volmit.iris.util.RollingSequence;
import com.volmit.iris.object.IrisBiome;
import com.volmit.iris.object.IrisRegion;
import com.volmit.iris.scaffold.engine.Engine;
import com.volmit.iris.util.*;
import io.netty.util.internal.ConcurrentSet;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import javax.swing.*;
import javax.swing.event.MouseInputListener;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
@ -48,8 +38,9 @@ public class MapVision extends JPanel {
private static final int DEF_HEIGHT = 820;
private IrisComplex complex;
private RenderType currentType = RenderType.BIOME_LAND;
private final Engine engine;
private final IrisComplex complex;
private RenderType currentType = RenderType.BIOME;
private int mouseX; //The current mouse coords
private int mouseY;
@ -61,36 +52,48 @@ public class MapVision extends JPanel {
private int offsetY;
private int lastTileWidth;
private boolean help = true;
private boolean helpIgnored = false;
private boolean dirty = true; //Whether to repaint textures
private double scale = 1;
private boolean realname = false;
private boolean shift = false;
private boolean alt = false;
private KMap<Integer, Tile> tiles = new KMap<>();
private final KMap<Integer, Tile> tiles = new KMap<>();
private Set<Tile> visibleTiles = new ConcurrentSet<>(); //Tiles that are visible on screen
private Set<Tile> halfDirtyTiles = new ConcurrentSet<>(); //Tiles that should be drawn next draw
private final Set<Tile> visibleTiles = new ConcurrentSet<>(); //Tiles that are visible on screen
private final Set<Tile> halfDirtyTiles = new ConcurrentSet<>(); //Tiles that should be drawn next draw
private short[][] spiral; //See #generateSpiral
// TODO, Why not use spiraler?
private final Color overlay = new Color(80, 80, 80);
private final Font overlayFont = new Font("Arial", Font.BOLD, 16);
private RollingSequence roll = new RollingSequence(50);
private final RollingSequence roll = new RollingSequence(50);
private boolean debug = false;
private int[] debugBorder = new int[] {-5, -3, 6, 4};
private final int[] debugBorder = new int[]{-5, -3, 6, 4};
private boolean recalculating;
// IrisComplex is the main class I need for a biome map. You can make one from an Engine object,
// which does need a FakeWorld object in it for the seed
public MapVision(IrisComplex worldComplex)
{
this.complex = worldComplex;
public MapVision(Engine ee) {
this.engine = ee;
this.complex = engine.getFramework().getComplex();
this.setBackground(Color.BLACK);
this.setVisible(true);
roll.put(1);
generateSpiral(64);
J.a(() -> {
J.sleep(10000);
if (!helpIgnored && help) {
help = false;
dirty = true;
}
});
addMouseWheelListener((mouseWheelEvent) -> {
double oldScale = this.scale;
@ -109,19 +112,61 @@ public class MapVision extends JPanel {
repaint();
softRecalculate();
});
addMouseMotionListener(new MouseMotionListener()
{
addMouseListener(new MouseInputListener() {
@Override
public void mouseMoved(MouseEvent e)
public void mouseClicked(MouseEvent e) {
if(shift)
{
Point cp = e.getPoint();
mouseX = cp.x;
mouseY = cp.y;
teleport();
}
else if (alt)
{
vscode();
}
}
@Override
public void mouseDragged(MouseEvent e)
{
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseDragged(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
});
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
Point cp = e.getPoint();
mouseX = cp.x;
mouseY = cp.y;
dirty = true;
}
@Override
public void mouseDragged(MouseEvent e) {
Point cp = e.getPoint();
draggedOffsetX -= (mouseX - cp.x) / scale;
draggedOffsetY -= (mouseY - cp.y) / scale;
@ -135,6 +180,66 @@ public class MapVision extends JPanel {
}
private void vscode() {
int windowOffsetX = getWidth() / 2;
int windowOffsetY = getHeight() / 2;
int x = (int) ((mouseX - windowOffsetX) - (draggedOffsetX)) << 2;
int y = (int) ((mouseY - windowOffsetY) - (draggedOffsetY)) << 2;
switch (currentType)
{
case BIOME, HEIGHT -> {
try {
File f = complex.getTrueBiomeStream().get(x,y).getLoadFile();
Desktop.getDesktop().open(f);
} catch (Throwable e) {
Iris.reportError(e);
}
}
case BIOME_LAND -> {
try {
File f = complex.getLandBiomeStream().get(x,y).getLoadFile();
Desktop.getDesktop().open(f);
} catch (Throwable e) {
Iris.reportError(e);
}
}
case BIOME_SEA -> {
try {
File f = complex.getSeaBiomeStream().get(x,y).getLoadFile();
Desktop.getDesktop().open(f);
} catch (Throwable e) {
Iris.reportError(e);
}
}
case REGION -> {
try {
File f = complex.getRegionStream().get(x,y).getLoadFile();
Desktop.getDesktop().open(f);
} catch (Throwable e) {
Iris.reportError(e);
}
}
case CAVE_LAND -> {
try {
File f = complex.getCaveBiomeStream().get(x,y).getLoadFile();
Desktop.getDesktop().open(f);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
}
private void teleport() {
int windowOffsetX = getWidth() / 2;
int windowOffsetY = getHeight() / 2;
int x = (int) ((mouseX - windowOffsetX) - (draggedOffsetX)) << 2;
int y = (int) ((mouseY - windowOffsetY) - (draggedOffsetY)) << 2;
}
public void redrawAll() {
}
/**
* Open this GUI
*/
@ -159,30 +264,105 @@ public class MapVision extends JPanel {
}
@Override
public void componentShown(ComponentEvent e) { }
public void componentShown(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) { }
public void componentHidden(ComponentEvent e) {
}
});
frame.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) { }
public void keyTyped(KeyEvent e) {
int currentMode = currentType.ordinal();
if (e.getKeyCode() == KeyEvent.VK_M) {
dirty = true;
currentType = RenderType.values()[(currentMode+1) % RenderType.values().length];
forceRecalculate();
return;
}
for(RenderType i : RenderType.values())
{
if (e.getKeyChar() == String.valueOf(i.ordinal()).charAt(0)) {
if(i.ordinal() != currentMode)
{
dirty = true;
currentType = i;
forceRecalculate();
return;
}
}
}
if (e.getKeyCode() == KeyEvent.VK_R) {
dirty = true;
forceRecalculate();
return;
}
if (e.getKeyCode() == KeyEvent.VK_EQUALS) {
double oldScale = MapVision.this.scale;
MapVision.this.scale = Math.min(4, Math.max(scale - 0.2, 1));
double wx = getWidth() / 2;
double hy = getHeight() / 2;
double xScale = (mouseX - wx) / wx;
double yScale = (mouseY - hy) / hy;
dirty = true;
repaint();
softRecalculate();
return;
}
if (e.getKeyCode() == KeyEvent.VK_MINUS) {
double oldScale = MapVision.this.scale;
MapVision.this.scale = Math.min(4, Math.max(scale + 0.2, 1));
double wx = getWidth() / 2;
double hy = getHeight() / 2;
double xScale = (mouseX - wx) / wx;
double yScale = (mouseY - hy) / hy;
dirty = true;
repaint();
softRecalculate();
return;
}
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT)
realname = true;
else if (e.getKeyCode() == KeyEvent.VK_ALT) debug = !debug;
else if (e.getKeyCode() == KeyEvent.VK_R) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
shift = true;
dirty = true;
} else if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) {
dirty = true;
debug = true;
} else if (e.getKeyCode() == KeyEvent.VK_SLASH) {
help = true;
helpIgnored = true;
dirty = true;
}else if (e.getKeyCode() == KeyEvent.VK_ALT) {
alt = true;
dirty = true;
repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT)
realname = false;
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) {
debug = false;
dirty = true;
} else if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
shift = false;
dirty = true;
} else if (e.getKeyCode() == KeyEvent.VK_SLASH) {
help = false;
helpIgnored = true;
dirty = true;
}else if (e.getKeyCode() == KeyEvent.VK_ALT) {
alt = false;
dirty = true;
}
}
});
File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png");
@ -190,7 +370,8 @@ public class MapVision extends JPanel {
if (file != null) {
try {
frame.setIconImage(ImageIO.read(file));
} catch(IOException ignored) { }
} catch (IOException ignored) {
}
}
frame.setVisible(true);
@ -240,21 +421,45 @@ public class MapVision extends JPanel {
}
gx.setColor(overlay);
gx.fillRect(getWidth() - 400, 4, 396, 27);
gx.fillRect(getWidth() - 400, 4, 396, 27 + (20 * (shift ? 2 : 1)));
gx.setColor(Color.WHITE);
//int x = (int) (((int) ((mouseX - windowOffsetX)) << 2) + (draggedOffsetX * scale));
//int y = (int) (((int) ((mouseY - windowOffsetY)) << 2) + (draggedOffsetY * scale));
int x = (int) (((int) ((mouseX - windowOffsetX))) - (draggedOffsetX)) << 2;
int y = (int) (((int) ((mouseY - windowOffsetY))) - (draggedOffsetY)) << 2;
String text = " [" + x+ ", " + y + "]";
if (realname)
text = complex.getLandBiomeStream().get(x, y).getLoadKey().toUpperCase() + text;
else
text = complex.getLandBiomeStream().get(x, y).getName().toUpperCase() + text;
gx.setFont(overlayFont);
gx.drawString(text, getWidth() - 400 + 6, 23);
int x = (int) ((mouseX - windowOffsetX) - (draggedOffsetX)) << 2;
int y = (int) ((mouseY - windowOffsetY) - (draggedOffsetY)) << 2;
if (debug) {
gx.setFont(overlayFont);
IrisBiome biome = complex.getLandBiomeStream().get(x, y);
int gg = 23;
gx.drawString(biome.getName().toUpperCase() + " [" + x + ", " + y + "]", getWidth() - 400 + 6, gg += 20);
if (shift) {
IrisRegion region = complex.getRegionStream().get(x, y);
gx.drawString("Region: " + region.getName(), getWidth() - 400 + 6, gg += 20);
}
if (help) {
gx.setColor(overlay);
gx.fillRect(10, 10, 470, 25 + (20 * (7 + (RenderType.values().length))));
gx.setColor(Color.WHITE);
int ggx = 25;
gx.drawString("/ to show this help screen", 20, ggx += 20);
gx.drawString("R to repaint the screen", 20, ggx += 20);
gx.drawString("+/- to Change Zoom", 20, ggx += 20);
gx.drawString("M to cycle render modes", 20, ggx += 20);
int ff = 0;
for (RenderType i : RenderType.values()) {
ff++;
gx.drawString(ff + " to view " + Form.capitalizeWords(i.name().toLowerCase().replaceAll("\\Q_\\E", " ")), 20, ggx += 20);
}
gx.drawString("Shift for additional biome details (at cursor)", 20, ggx += 20);
gx.drawString("Shift + Click to teleport to location", 20, ggx += 20);
gx.drawString("Alt + Click to open biome in VSCode", 20, ggx += 20);
} else if (debug) {
gx.setColor(Color.RED);
int xx = (int) Math.round((debugBorder[0] << TILE_SIZE_R) / scale + offsetX);
int yy = (int) Math.round((debugBorder[1] << TILE_SIZE_R) / scale + offsetY);
@ -276,8 +481,8 @@ public class MapVision extends JPanel {
gx.drawString("Tiles (Visible)" + visibleTiles.size(), 20, 125);
gx.drawString("Tiles (Total) " + tiles.size(), 20, 145);
x = (int) (((int) ((mouseX - windowOffsetX))) + (-draggedOffsetX * scale)) >> TILE_SIZE_R;
y = (int) (((int) ((mouseY - windowOffsetY))) + (-draggedOffsetY * scale)) >> TILE_SIZE_R;
x = (int) ((mouseX - windowOffsetX) + (-draggedOffsetX * scale)) >> TILE_SIZE_R;
y = (int) ((mouseY - windowOffsetY) + (-draggedOffsetY * scale)) >> TILE_SIZE_R;
Tile t = getTile((short) x, (short) y);
boolean b1 = t != null;
boolean b2 = b1 && visibleTiles.contains(t);
@ -308,13 +513,10 @@ public class MapVision extends JPanel {
gx.drawImage(tile.getImage(), x, y, size + off, size + off, null);
}
private Runnable sleepTask = new Runnable() {
@Override
public void run() {
double t = Math.max(Math.min(roll.getAverage(), 1000), 30);
private final Runnable sleepTask = () -> {
double t = Math.max(Math.min(roll.getAverage(), 100), 5);
J.sleep((long) t);
repaint();
}
};
/**
@ -333,6 +535,12 @@ public class MapVision extends JPanel {
centerTileY = y;
}
public void forceRecalculate()
{
dirty = true;
tiles.clear();
}
/**
* Recalculate what tiles should be visible on screen, as well as queue
* new tiles to be created
@ -397,11 +605,9 @@ public class MapVision extends JPanel {
Tile t = tiles.get(id);
toRemove.remove(t); //Make sure this tile isn't removed
if (!visibleTiles.contains(t)) {
visibleTiles.add(t); //Make sure it's visible again if it isn't
}
}
}
queueForRemoval(toRemove); //Queue all tiles not on screen for removal
@ -411,6 +617,7 @@ public class MapVision extends JPanel {
/**
* Queue a tile for creation
*
* @param tileX X tile coord
* @param tileY Y tile coord
*/
@ -449,7 +656,7 @@ public class MapVision extends JPanel {
@Override
public void run() {
Tile tile = new Tile(tileX, tileY);
tile.render(complex, currentType);
tile.render(engine, currentType);
tiles.put(getTileId(tileX, tileY), tile);
visibleTiles.add(tile);
//dirty = true; //Disabled marking as dirty so a redraw of the entire map isn't needed
@ -484,6 +691,7 @@ public class MapVision extends JPanel {
/**
* Get a tile based on the X and Z coords of the tile
*
* @param tileX X Coord
* @param tileY Y Coord
* @return
@ -495,6 +703,7 @@ public class MapVision extends JPanel {
/**
* Get an integer that represents a tile's location
*
* @param tileX X Coord
* @param tileY Y Coord
* @return
@ -505,6 +714,7 @@ public class MapVision extends JPanel {
/**
* Converts an integer representing a tiles location back into 2 shorts
*
* @param id The tile integer
* @return
*/
@ -515,6 +725,7 @@ public class MapVision extends JPanel {
/**
* Generates a 2D array of relative tile locations. This is so we know what order
* to search for new tiles in a nice, spiral way
*
* @param size Size of the array
*/
public void generateSpiral(int size) {
@ -532,23 +743,27 @@ public class MapVision extends JPanel {
y = ((int) (size / 2.0)) - 1;
int offset = (size / 2) - 1;
for (int k=1; k<=(size-1); k++)
{
for (int j=0; j<(k<(size-1)?2:3); j++)
{
for (int i=0; i<s; i++)
{
for (int k = 1; k <= (size - 1); k++) {
for (int j = 0; j < (k < (size - 1) ? 2 : 3); j++) {
for (int i = 0; i < s; i++) {
short[] coords = {(short) (x - offset), (short) (y - offset)};
newSpiral[c] = coords;
c++;
//Iris.info("Spiral " + coords[0] + ", " + coords[1]); //Testing
switch (d)
{
case 0: y = y + 1; break;
case 1: x = x + 1; break;
case 2: y = y - 1; break;
case 3: x = x - 1; break;
switch (d) {
case 0:
y = y + 1;
break;
case 1:
x = x + 1;
break;
case 2:
y = y - 1;
break;
case 3:
x = x - 1;
break;
}
}
d = (d + 1) % 4;
@ -573,7 +788,7 @@ public class MapVision extends JPanel {
return t;
});*/
private ThreadFactory factory = new ThreadFactory() {
private final ThreadFactory factory = new ThreadFactory() {
@Override
public Thread newThread(@NotNull Runnable r) {
threadId++;

View File

@ -1,5 +1,5 @@
package com.volmit.iris.map;
public enum RenderType {
BIOME_LAND, REGION, CAVE_LAND, HEIGHT
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, LAYER_LOAD
}

View File

@ -3,6 +3,7 @@ package com.volmit.iris.map;
import com.volmit.iris.generator.IrisComplex;
import com.volmit.iris.object.IrisBiome;
import com.volmit.iris.object.IrisRegion;
import com.volmit.iris.scaffold.engine.Engine;
import com.volmit.iris.scaffold.stream.ProceduralStream;
import lombok.Getter;
import lombok.Setter;
@ -49,27 +50,25 @@ public class Tile {
/**
* Render the tile
* @param complex The world complex
* @param type The type of render
* @return True when rendered
*/
public boolean render(IrisComplex complex, RenderType type) {
public boolean render(Engine engine, RenderType type) {
BufferedImage newImage = new BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB);
BiFunction<Integer, Integer, Integer> colorFunction = (integer, integer2) -> Color.black.getRGB();
BiFunction<Integer, Integer, Integer> getColor;
if (type == RenderType.BIOME_LAND) {
getColor = (x, z) -> complex.getLandBiomeStream().get(x, z).getColor().getRGB();
} else if (type == RenderType.REGION) {
getColor = (x, z) -> complex.getRegionStream().get(x, z).getColor(complex).getRGB();
} else if (type == RenderType.HEIGHT) {
getColor = (x, z) -> Color.getHSBColor(complex.getHeightStream().get(x, z).floatValue(), 100, 100).getRGB();
} else {
getColor = (x, z) -> complex.getCaveBiomeStream().get(x, z).getColor().getRGB();
switch (type) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> colorFunction = (x, z) -> engine.getFramework().getComplex().getTrueBiomeStream().get(x, z).getColor(engine, type).getRGB();
case BIOME_LAND -> colorFunction = (x, z) -> engine.getFramework().getComplex().getLandBiomeStream().get(x, z).getColor(engine, type).getRGB();
case BIOME_SEA -> colorFunction = (x, z) -> engine.getFramework().getComplex().getSeaBiomeStream().get(x, z).getColor(engine, type).getRGB();
case REGION -> colorFunction = (x, z) -> engine.getFramework().getComplex().getRegionStream().get(x, z).getColor(engine.getFramework().getComplex(), type).getRGB();
case CAVE_LAND -> colorFunction = (x, z) -> engine.getFramework().getComplex().getCaveBiomeStream().get(x, z).getColor(engine, type).getRGB();
case HEIGHT -> colorFunction = (x, z) -> Color.getHSBColor(engine.getFramework().getComplex().getHeightStream().get(x, z).floatValue(), 100, 100).getRGB();
}
for (int i = 0; i < 128; i++) {
for (int j = 0; j < 128; j++) {
newImage.setRGB(i, j, getColor.apply(translate(x, i), translate(y, j)));
newImage.setRGB(i, j, colorFunction.apply(translate(x, i), translate(y, j)));
}
}
image = newImage;