// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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 heightmapByLoadNum = new HashMap<>(); public static final HashMap _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 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; } }