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.
439 lines
14 KiB
439 lines
14 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.Bounds; |
|
import engine.math.FastMath; |
|
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; |
|
|
|
import static java.lang.Math.abs; |
|
|
|
public class HeightMap { |
|
|
|
// Class variables |
|
|
|
public static final HashMap<Integer, HeightMap> heightmapByLoadNum = new HashMap<>(); |
|
|
|
public static final HashMap<String, int[][]> _pixelData = new HashMap<>(); |
|
|
|
// Heightmap data for all zones. |
|
public static float SCALEVALUE = 1.0f / 255; |
|
|
|
// Bootstrap Tracking |
|
public static int heightMapsCreated = 0; |
|
public static HeightMap PlayerCityHeightMap; |
|
|
|
// 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 bucketWidthX; |
|
public float bucketWidthY; |
|
public float seaLevel = 0; |
|
public int[][] pixelColorValues; |
|
|
|
public float zone_minBlend; |
|
public float zone_maxBlend; |
|
|
|
public HeightMap(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"); |
|
|
|
this.zone_minBlend = rs.getFloat("outsetZ"); |
|
this.zone_maxBlend = rs.getFloat("outsetX"); |
|
|
|
// 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 |
|
|
|
float numOfBucketsX = this.heightmapImage.getWidth() - 1; |
|
float calculatedWidthX = this.fullExtentsX / numOfBucketsX; |
|
this.bucketWidthX = calculatedWidthX; |
|
|
|
float numOfBucketsY = this.heightmapImage.getHeight() - 1; |
|
float calculatedWidthY = this.fullExtentsY / numOfBucketsY; |
|
this.bucketWidthY = calculatedWidthY; |
|
|
|
// Generate pixel array from image data |
|
|
|
generatePixelData(this); |
|
|
|
HeightMap.heightmapByLoadNum.put(this.zoneLoadID, this); |
|
|
|
heightMapsCreated++; |
|
} |
|
|
|
//Created for PlayerCities |
|
public HeightMap() { |
|
|
|
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; |
|
this.zone_minBlend = 0; |
|
this.zone_maxBlend = 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.bucketWidthX = halfExtentsX; |
|
this.bucketWidthY = halfExtentsY; |
|
|
|
this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY]; |
|
|
|
for (int y = 0; y < this.fullExtentsY; y++) { |
|
for (int x = 0; x < this.fullExtentsX; x++) { |
|
pixelColorValues[x][y] = 255; |
|
} |
|
} |
|
|
|
} |
|
|
|
public HeightMap(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.bucketWidthX = halfExtentsX; |
|
this.bucketWidthY = halfExtentsY; |
|
|
|
this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY]; |
|
|
|
for (int y = 0; y < this.fullExtentsY; y++) { |
|
for (int x = 0; x < this.fullExtentsX; x++) { |
|
pixelColorValues[x][y] = 0; |
|
} |
|
} |
|
|
|
} |
|
|
|
public static void GeneratePlayerCityHeightMap() { |
|
|
|
HeightMap.PlayerCityHeightMap = new HeightMap(); |
|
|
|
} |
|
|
|
public static void GenerateCustomHeightMap(Zone zone) { |
|
|
|
HeightMap heightMap = new HeightMap(zone); |
|
|
|
HeightMap.heightmapByLoadNum.put(zone.zoneTemplate, heightMap); |
|
|
|
} |
|
|
|
public static Zone getNextZoneWithTerrain(Zone zone) { |
|
|
|
Zone nextZone = zone; |
|
|
|
if (zone.getHeightMap() != null) |
|
return zone; |
|
|
|
if (zone.equals(ZoneManager.getSeaFloor())) |
|
return zone; |
|
|
|
while (nextZone.getHeightMap() == null) |
|
nextZone = nextZone.parent; |
|
|
|
return nextZone; |
|
} |
|
|
|
public static float getWorldHeight(Zone currentZone, Vector3fImmutable worldLoc) { |
|
|
|
Zone heightMapZone; |
|
|
|
// 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. |
|
|
|
heightMapZone = getNextZoneWithTerrain(currentZone); |
|
|
|
// Transform world loc into zone space coordinate system |
|
|
|
Vector2f zoneLoc = ZoneManager.worldToZoneSpace(worldLoc, heightMapZone); |
|
|
|
// Interpolate height for this position using pixel array. |
|
|
|
float interpolatedTerrainHeight = heightMapZone.getHeightMap().getInterpolatedTerrainHeight(zoneLoc); |
|
interpolatedTerrainHeight += heightMapZone.worldAltitude; |
|
|
|
// Heightmap blending is based on distance to edge of zone. |
|
|
|
if (Bounds.collide(worldLoc, heightMapZone.blendBounds) == true) |
|
return interpolatedTerrainHeight; |
|
|
|
// We will need the parent height if we got this far into the method |
|
|
|
return interpolatePLANAR(worldLoc, heightMapZone, zoneLoc, interpolatedTerrainHeight); |
|
|
|
} |
|
|
|
private static float interpolatePLANAR(Vector3fImmutable worldLoc, Zone heightMapZone, Vector2f zoneLoc, float interpolatedTerrainHeight) { |
|
|
|
// zones of type PLANAR |
|
|
|
Zone parentZone; |
|
float interpolatedParentTerrainHeight; |
|
|
|
parentZone = HeightMap.getNextZoneWithTerrain(heightMapZone.parent); |
|
|
|
interpolatedParentTerrainHeight = HeightMap.getWorldHeight(parentZone, worldLoc); |
|
interpolatedParentTerrainHeight += parentZone.worldAltitude; |
|
|
|
Bounds blendBounds = Bounds.borrow(); |
|
zoneLoc.x = abs(zoneLoc.x); |
|
zoneLoc.y = abs(zoneLoc.x); |
|
|
|
blendBounds.setBounds(new Vector2f(heightMapZone.absX, heightMapZone.absZ), zoneLoc, 0.0f); |
|
|
|
float maxBlendArea = (heightMapZone.blendBounds.getHalfExtents().x) * |
|
(heightMapZone.blendBounds.getHalfExtents().y); |
|
float currentArea = (blendBounds.getHalfExtents().x) * |
|
(blendBounds.getHalfExtents().y); |
|
float zoneArea = (heightMapZone.bounds.getHalfExtents().x) * |
|
(heightMapZone.bounds.getHalfExtents().y); |
|
|
|
blendBounds.release(); |
|
|
|
float blendDelta = zoneArea - maxBlendArea; |
|
float currentDelta = zoneArea - currentArea; |
|
|
|
float percentage; |
|
|
|
if (currentDelta != 0 && blendDelta != 0) |
|
percentage = currentDelta / blendDelta; |
|
else |
|
percentage = 0.0f; |
|
|
|
interpolatedTerrainHeight = FastMath.LERP(percentage, interpolatedTerrainHeight, interpolatedParentTerrainHeight); |
|
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. |
|
|
|
HeightMap.GeneratePlayerCityHeightMap(); |
|
|
|
// Clear all heightmap image data as it's no longer needed. |
|
|
|
for (HeightMap heightMap : HeightMap.heightmapByLoadNum.values()) { |
|
heightMap.heightmapImage = null; |
|
} |
|
|
|
Logger.info(HeightMap.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 for this heightmap. RPG channels are all the same |
|
// in this greyscale TGA heightmap. We will choose red. |
|
|
|
int[][] colorData = new int[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] = color.getRed(); |
|
} |
|
} |
|
|
|
// Insert color data into lookup table |
|
|
|
_pixelData.put(imageFile.getName().substring(0, imageFile.getName().lastIndexOf(".")), colorData); |
|
|
|
} catch (IOException e) { |
|
Logger.error(e); |
|
} |
|
|
|
|
|
} |
|
}); |
|
} catch (IOException e) { |
|
Logger.error(e); |
|
} |
|
|
|
} |
|
|
|
private static void generatePixelData(HeightMap heightMap) { |
|
|
|
Color color; |
|
|
|
// Generate altitude lookup table for this heightmap |
|
|
|
heightMap.pixelColorValues = new int[heightMap.heightmapImage.getWidth()][heightMap.heightmapImage.getHeight()]; |
|
|
|
for (int y = 0; y < heightMap.heightmapImage.getHeight(); y++) { |
|
for (int x = 0; x < heightMap.heightmapImage.getWidth(); x++) { |
|
|
|
color = new Color(heightMap.heightmapImage.getRGB(x, y)); |
|
heightMap.pixelColorValues[x][y] = color.getRed(); |
|
} |
|
} |
|
|
|
} |
|
|
|
public Vector2f getGridSquare(Vector2f zoneLoc) { |
|
|
|
// Clamp values. |
|
|
|
if (zoneLoc.x < 0) |
|
zoneLoc.setX(0); |
|
|
|
if (zoneLoc.x >= this.fullExtentsX) |
|
zoneLoc.setX(this.fullExtentsX); |
|
|
|
if (zoneLoc.y < 0) |
|
zoneLoc.setY(0); |
|
|
|
if (zoneLoc.y > this.fullExtentsY) |
|
zoneLoc.setY(this.fullExtentsY); |
|
|
|
// Flip Y coordinates |
|
|
|
zoneLoc.setY(this.fullExtentsY - zoneLoc.y); |
|
|
|
float xBucket = (zoneLoc.x / this.bucketWidthX); |
|
float yBucket = (zoneLoc.y / this.bucketWidthY); |
|
|
|
return new Vector2f(xBucket, yBucket); |
|
} |
|
|
|
public float getInterpolatedTerrainHeight(Vector2f zoneLoc) { |
|
|
|
float interpolatedHeight; |
|
|
|
Vector2f gridSquare = getGridSquare(zoneLoc); |
|
|
|
int gridX = (int) gridSquare.x; |
|
int gridY = (int) gridSquare.y; |
|
|
|
//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 |
|
|
|
float offsetX = (gridSquare.x - gridX); |
|
float offsetY = gridSquare.y - gridY; |
|
|
|
interpolatedHeight = topRightHeight * (1 - offsetY) * (offsetX); |
|
interpolatedHeight += (bottomRightHeight * offsetY * offsetX); |
|
interpolatedHeight += (bottomLeftHeight * (1 - offsetX) * offsetY); |
|
interpolatedHeight += (topLeftHeight * (1 - offsetX) * (1 - offsetY)); |
|
|
|
interpolatedHeight *= (float) this.maxHeight * SCALEVALUE; // Scale height |
|
|
|
return interpolatedHeight; |
|
} |
|
|
|
}
|
|
|