// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; public class HeightMap { // Class variables public static float SCALEVALUE = 1.0f / 256; // Heightmap data for all zones. public static final HashMap heightmapByLoadNum = new HashMap<>(); // Bootstrap Tracking public static int heightMapsCreated = 0; public static HeightMap PlayerCityHeightMap; // Heightmap data for this heightmap public BufferedImage heightmapImage; private final int heightMapID; private final int maxHeight; private final int fullExtentsX; private final int fullExtentsY; private float bucketWidthX; private float bucketWidthY; private final int zoneLoadID; private float seaLevel = 0; private int[][] pixelColorValues; 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"); // 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.extents; int halfExtentsY = (int) Enum.CityBoundsType.ZONE.extents; 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] = 255; } } } public HeightMap(Zone zone) { this.heightMapID = 999999; this.maxHeight = 0; int halfExtentsX = (int) zone.getBounds().getHalfExtents().x; int halfExtentsY = (int) zone.getBounds().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.getLoadNum(), 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.getParent(); 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); // Position returned from Heightmap engine is relative to zone world height return interpolatedTerrainHeight + heightMapZone.worldAltitude; } 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."); } public static boolean isLocUnderwater(Vector3fImmutable currentLoc) { float localAltitude = HeightMap.getWorldHeight(currentLoc); Zone zone = ZoneManager.findSmallestZone(currentLoc); return localAltitude < zone.getSeaLevel(); } 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) { 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) { Vector2f gridSquare; if (zoneLoc.x < 0) zoneLoc.x = 0; if (zoneLoc.y < 0) zoneLoc.y = 0; if (zoneLoc.x > this.fullExtentsX) zoneLoc.x = this.fullExtentsX; if (zoneLoc.y > this.fullExtentsY) zoneLoc.y = this.fullExtentsY; int maxX = (int) (this.fullExtentsX / this.bucketWidthX); int maxY = (int) (this.fullExtentsY / this.bucketWidthY); gridSquare = getGridSquare(zoneLoc); int gridX = (int) gridSquare.x; int gridY = (int) gridSquare.y; if (gridX < 0) gridX = 0; if (gridY < 0) gridY = 0; if (gridX >= maxX) gridX = maxX - 1; if (gridY >= maxY) gridY = maxY - 1; float offsetX = (gridSquare.x - gridX); float offsetY = gridSquare.y - gridY; //get height of the 4 vertices. float topLeftHeight = 0; float topRightHeight = 0; float bottomLeftHeight = 0; float bottomRightHeight = 0; int nextY = gridY + 1; int nextX = gridX + 1; if (nextY > maxY) nextY = gridY; if (nextX > maxX) nextX = gridX; topLeftHeight = pixelColorValues[gridX][gridY]; topRightHeight = pixelColorValues[nextX][gridY]; bottomLeftHeight = pixelColorValues[gridX][nextY]; bottomRightHeight = pixelColorValues[nextX][nextY]; float interpolatedHeight; 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; } public float getBucketWidthX() { return bucketWidthX; } public float getBucketWidthY() { return bucketWidthY; } public int getHeightMapID() { return heightMapID; } public BufferedImage getHeightmapImage() { return heightmapImage; } public float getSeaLevel() { return seaLevel; } }