forked from MagicBane/Server
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
12 KiB
371 lines
12 KiB
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . |
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· |
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ |
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ |
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ |
|
// Magicbane Emulator Project © 2013 - 2022 |
|
// www.magicbane.com |
|
|
|
package engine.InterestManagement; |
|
|
|
import engine.Enum; |
|
import engine.gameManager.ConfigManager; |
|
import engine.gameManager.DbManager; |
|
import engine.gameManager.ZoneManager; |
|
import engine.math.Vector2f; |
|
import engine.math.Vector3fImmutable; |
|
import engine.objects.Zone; |
|
import org.pmw.tinylog.Logger; |
|
|
|
import javax.imageio.ImageIO; |
|
import java.awt.*; |
|
import java.awt.image.BufferedImage; |
|
import java.io.File; |
|
import java.io.IOException; |
|
import java.nio.file.Files; |
|
import java.nio.file.Path; |
|
import java.nio.file.Paths; |
|
import java.sql.ResultSet; |
|
import java.sql.SQLException; |
|
import java.util.HashMap; |
|
import java.util.stream.Stream; |
|
|
|
public class Terrain { |
|
|
|
// Class variables |
|
|
|
public static final HashMap<Integer, Terrain> heightmapByLoadNum = new HashMap<>(); |
|
|
|
public static final HashMap<Integer, short[][]> _pixelData = new HashMap<>(); |
|
|
|
// Bootstrap Tracking |
|
public static int heightMapsCreated = 0; |
|
public static Terrain playerCityTerrain; |
|
|
|
// Heightmap data for this heightmap |
|
public final int heightMapID; |
|
public final int maxHeight; |
|
public final int fullExtentsX; |
|
public final int fullExtentsY; |
|
public final int zoneLoadID; |
|
public BufferedImage heightmapImage; |
|
public float cell_size_x; |
|
public float cell_size_y; |
|
public float seaLevel = 0; |
|
public short[][] pixelColorValues; |
|
public int cell_count_x; |
|
public int cell_count_y; |
|
|
|
public float terrain_scale; |
|
|
|
public Terrain(ResultSet rs) throws SQLException { |
|
|
|
this.heightMapID = rs.getInt("heightMapID"); |
|
this.maxHeight = rs.getInt("maxHeight"); |
|
int halfExtentsX = rs.getInt("xRadius"); |
|
int halfExtentsY = rs.getInt("zRadius"); |
|
this.zoneLoadID = rs.getInt("zoneLoadID"); |
|
this.seaLevel = rs.getFloat("seaLevel"); |
|
|
|
// Cache the full extents to avoid the calculation |
|
|
|
this.fullExtentsX = halfExtentsX * 2; |
|
this.fullExtentsY = halfExtentsY * 2; |
|
|
|
this.heightmapImage = null; |
|
File imageFile = new File(ConfigManager.DEFAULT_DATA_DIR + "heightmaps/" + this.heightMapID + ".bmp"); |
|
|
|
// early exit if no image file was found. Will log in caller. |
|
|
|
if (!imageFile.exists()) |
|
return; |
|
|
|
// load the heightmap image. |
|
|
|
try { |
|
this.heightmapImage = ImageIO.read(imageFile); |
|
} catch (IOException e) { |
|
Logger.error("***Error loading heightmap data for heightmap " + this.heightMapID + e); |
|
} |
|
|
|
// Calculate the data we do not load from table |
|
|
|
this.cell_count_x = this.heightmapImage.getWidth() - 1; |
|
this.cell_size_x = this.fullExtentsX / (float) cell_count_x; |
|
|
|
this.cell_count_y = this.heightmapImage.getHeight() - 1; |
|
this.cell_size_y = this.fullExtentsY / (float) cell_count_y; |
|
|
|
this.terrain_scale = this.maxHeight / 255f; |
|
|
|
// Generate pixel array from image data |
|
|
|
generatePixelData(this); |
|
|
|
Terrain.heightmapByLoadNum.put(this.zoneLoadID, this); |
|
|
|
heightMapsCreated++; |
|
} |
|
|
|
//Created for PlayerCities |
|
public Terrain() { |
|
|
|
this.heightMapID = 999999; |
|
this.maxHeight = 5; // for real... |
|
int halfExtentsX = (int) Enum.CityBoundsType.ZONE.halfExtents; |
|
int halfExtentsY = (int) Enum.CityBoundsType.ZONE.halfExtents; |
|
this.zoneLoadID = 0; |
|
this.seaLevel = 0; |
|
|
|
// Cache the full extents to avoid the calculation |
|
|
|
this.fullExtentsX = halfExtentsX * 2; |
|
this.fullExtentsY = halfExtentsY * 2; |
|
|
|
this.heightmapImage = null; |
|
|
|
// Calculate the data we do not load from table |
|
|
|
this.cell_size_x = halfExtentsX; |
|
this.cell_size_y = halfExtentsY; |
|
|
|
this.pixelColorValues = new short[this.fullExtentsX][this.fullExtentsY]; |
|
|
|
for (int y = 0; y < this.fullExtentsY; y++) { |
|
for (int x = 0; x < this.fullExtentsX; x++) { |
|
pixelColorValues[x][y] = 255; |
|
} |
|
} |
|
|
|
cell_count_x = this.pixelColorValues.length - 1; |
|
cell_count_y = this.pixelColorValues[0].length - 1; |
|
this.terrain_scale = this.maxHeight / 255f; |
|
} |
|
|
|
public Terrain(Zone zone) { |
|
|
|
this.heightMapID = 999999; |
|
this.maxHeight = 0; |
|
int halfExtentsX = (int) zone.bounds.getHalfExtents().x; |
|
int halfExtentsY = (int) zone.bounds.getHalfExtents().y; |
|
this.zoneLoadID = 0; |
|
this.seaLevel = 0; |
|
|
|
// Cache the full extents to avoid the calculation |
|
|
|
this.fullExtentsX = halfExtentsX * 2; |
|
this.fullExtentsY = halfExtentsY * 2; |
|
|
|
this.heightmapImage = null; |
|
|
|
// Calculate the data we do not load from table |
|
|
|
this.cell_size_x = halfExtentsX; |
|
this.cell_size_y = halfExtentsY; |
|
|
|
this.pixelColorValues = new short[this.fullExtentsX][this.fullExtentsY]; |
|
|
|
for (int y = 0; y < this.fullExtentsY; y++) { |
|
for (int x = 0; x < this.fullExtentsX; x++) { |
|
pixelColorValues[x][y] = 0; |
|
} |
|
} |
|
|
|
cell_count_x = this.pixelColorValues.length - 1; |
|
cell_count_y = this.pixelColorValues[0].length - 1; |
|
this.terrain_scale = this.maxHeight / 255f; |
|
} |
|
|
|
public static void GeneratePlayerCityHeightMap() { |
|
|
|
Terrain.playerCityTerrain = new Terrain(); |
|
|
|
} |
|
|
|
public static void GenerateCustomHeightMap(Zone zone) { |
|
|
|
Terrain heightMap = new Terrain(zone); |
|
|
|
Terrain.heightmapByLoadNum.put(zone.template, heightMap); |
|
|
|
} |
|
|
|
public static Zone getNextZoneWithTerrain(Zone zone) { |
|
|
|
Zone terrain_zone = zone; |
|
|
|
if (zone.getHeightMap() != null) |
|
return zone; |
|
|
|
if (zone.equals(ZoneManager.getSeaFloor())) |
|
return zone; |
|
|
|
while (terrain_zone.getHeightMap() == null) |
|
terrain_zone = terrain_zone.parent; |
|
|
|
return terrain_zone; |
|
} |
|
|
|
public static float getWorldHeight(Zone currentZone, Vector3fImmutable worldLoc) { |
|
|
|
Zone terrainZone; |
|
|
|
// Seafloor is rather flat. |
|
|
|
if (currentZone == ZoneManager.getSeaFloor()) |
|
return currentZone.worldAltitude; |
|
|
|
// Retrieve the next zone with a heightmap attached. |
|
// Zones without a heightmap use the next zone up the |
|
// tree to calculate heights from. |
|
|
|
terrainZone = getNextZoneWithTerrain(currentZone); |
|
|
|
// Transform world loc into zone space coordinate system |
|
|
|
Vector2f terrainLoc = ZoneManager.worldToZoneSpace(worldLoc, terrainZone); |
|
|
|
// Interpolate height for this position using pixel array. |
|
|
|
float interpolatedTerrainHeight = terrainZone.getHeightMap().getInterpolatedTerrainHeight(terrainLoc); |
|
interpolatedTerrainHeight += terrainZone.worldAltitude; |
|
|
|
return interpolatedTerrainHeight; |
|
|
|
} |
|
|
|
public static float getWorldHeight(Vector3fImmutable worldLoc) { |
|
|
|
Zone currentZone = ZoneManager.findSmallestZone(worldLoc); |
|
|
|
if (currentZone == null) |
|
return 0; |
|
|
|
return getWorldHeight(currentZone, worldLoc); |
|
|
|
} |
|
|
|
public static void loadAlHeightMaps() { |
|
|
|
// Load the heightmaps into staging hashmap keyed by HashMapID |
|
|
|
DbManager.HeightMapQueries.LOAD_ALL_HEIGHTMAPS(); |
|
|
|
//generate static player city heightmap. |
|
|
|
Terrain.GeneratePlayerCityHeightMap(); |
|
|
|
Logger.info(Terrain.heightmapByLoadNum.size() + " Heightmaps cached."); |
|
|
|
// Load pixel data for heightmaps |
|
|
|
try (Stream<Path> filePathStream = Files.walk(Paths.get(ConfigManager.DEFAULT_DATA_DIR + "heightmaps/TARGA/"))) { |
|
filePathStream.forEach(filePath -> { |
|
|
|
if (Files.isRegularFile(filePath)) { |
|
File imageFile = filePath.toFile(); |
|
|
|
try { |
|
BufferedImage heightmapImage = ImageIO.read(imageFile); |
|
|
|
// Generate pixel data for this heightmap. RPG channels are all the same |
|
// in this greyscale TGA heightmap. We will choose red. |
|
|
|
short[][] colorData = new short[heightmapImage.getWidth()][heightmapImage.getHeight()]; |
|
|
|
for (int y = 0; y < heightmapImage.getHeight(); y++) |
|
for (int x = 0; x < heightmapImage.getWidth(); x++) { |
|
|
|
Color color = new Color(heightmapImage.getRGB(x, y)); |
|
colorData[x][y] = (short) color.getRed(); |
|
} |
|
|
|
// Insert color data into lookup table |
|
|
|
int heightMapID = Integer.parseInt(imageFile.getName().substring(0, imageFile.getName().lastIndexOf("."))); |
|
_pixelData.put(heightMapID, colorData); |
|
|
|
} catch (IOException e) { |
|
Logger.error(e); |
|
} |
|
|
|
} |
|
}); // Try with resources block |
|
|
|
} catch (IOException e) { |
|
Logger.error(e); |
|
} |
|
|
|
} |
|
|
|
private static void generatePixelData(Terrain terrain) { |
|
|
|
Color color; |
|
|
|
// Generate altitude lookup table for this heightmap |
|
|
|
terrain.pixelColorValues = new short[terrain.heightmapImage.getWidth()][terrain.heightmapImage.getHeight()]; |
|
|
|
for (int y = 0; y < terrain.heightmapImage.getHeight(); y++) { |
|
for (int x = 0; x < terrain.heightmapImage.getWidth(); x++) { |
|
|
|
color = new Color(terrain.heightmapImage.getRGB(x, y)); |
|
terrain.pixelColorValues[x][y] = (short) color.getRed(); |
|
} |
|
} |
|
|
|
} |
|
|
|
public Vector2f getTerrainCell(Vector2f terrainLoc) { |
|
|
|
float terrainCell_x = terrainLoc.x / this.cell_size_x; |
|
float terrainCell_y = terrainLoc.y / this.cell_size_y; |
|
|
|
// Clamp values when standing directly on max pole |
|
|
|
if (terrainCell_x >= this.cell_count_x) |
|
terrainCell_x = terrainCell_x - 1; |
|
|
|
if (terrainCell_y >= this.cell_count_y) |
|
terrainCell_y = terrainCell_x - 1; |
|
|
|
return new Vector2f(terrainCell_x, terrainCell_y); |
|
} |
|
|
|
public float getInterpolatedTerrainHeight(Vector2f terrainLoc) { |
|
|
|
float interpolatedHeight; |
|
|
|
Vector2f terrain_cell = getTerrainCell(terrainLoc); |
|
|
|
int gridX = (int) Math.floor(terrain_cell.x); |
|
int gridY = (int) Math.floor(terrain_cell.y); |
|
|
|
float offsetX = terrain_cell.x % 1; |
|
float offsetY = terrain_cell.y % 1; |
|
|
|
//get 4 surrounding vertices from the pixel array. |
|
|
|
float topLeftHeight; |
|
float topRightHeight; |
|
float bottomLeftHeight; |
|
float bottomRightHeight; |
|
|
|
topLeftHeight = pixelColorValues[gridX][gridY]; |
|
topRightHeight = pixelColorValues[gridX + 1][gridY]; |
|
bottomLeftHeight = pixelColorValues[gridX][gridY + 1]; |
|
bottomRightHeight = pixelColorValues[gridX + 1][gridY + 1]; |
|
|
|
// Interpolate between the 4 vertices |
|
|
|
interpolatedHeight = topRightHeight * (1 - offsetY) * (offsetX); |
|
interpolatedHeight += (bottomRightHeight * offsetY * offsetX); |
|
interpolatedHeight += (bottomLeftHeight * (1 - offsetX) * offsetY); |
|
interpolatedHeight += (topLeftHeight * (1 - offsetX) * (1 - offsetY)); |
|
|
|
interpolatedHeight *= this.terrain_scale; // Scale height |
|
|
|
return interpolatedHeight; |
|
} |
|
|
|
}
|
|
|