diff --git a/src/main/java/com/projectswg/common/data/swgiff/parsers/terrain/TerrainTemplate.kt b/src/main/java/com/projectswg/common/data/swgiff/parsers/terrain/TerrainTemplate.kt index a1a671c..372d723 100644 --- a/src/main/java/com/projectswg/common/data/swgiff/parsers/terrain/TerrainTemplate.kt +++ b/src/main/java/com/projectswg/common/data/swgiff/parsers/terrain/TerrainTemplate.kt @@ -1,3 +1,28 @@ +/*********************************************************************************** + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is an emulation project for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create one or more emulators which will provide servers for * + * players to continue playing a game similar to the one they used to play. * + * * + * This file is part of PSWGCommon. * + * * + * --------------------------------------------------------------------------------* + * * + * PSWGCommon is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * PSWGCommon is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with PSWGCommon. If not, see . * + ***********************************************************************************/ package com.projectswg.common.data.swgiff.parsers.terrain import com.projectswg.common.data.location.Point2f @@ -13,7 +38,7 @@ import kotlin.math.min import kotlin.math.sqrt class TerrainTemplate : SWGParser { - + var name = "" var mapWidth = 0f var chunkWidth = 0f @@ -44,16 +69,16 @@ class TerrainTemplate : SWGParser { var farRadialTileBorder = 0f var farRadialSeed = 0 var legacyMap = true - + val fractalGroup = FractalGroup() val bitmapGroup = BitmapGroup() private val lookupInformation = TerrainInfoLookup(fractalGroup.fractals, bitmapGroup.bitmaps) private val topTerrainLayer = TerrainListLayer() - + override fun read(form: IffForm) { assert(form.tag == "PTAT") // all versions are supported - + form.readChunk("DATA").use { chunk -> name = chunk.readString() mapWidth = chunk.readFloat() @@ -64,7 +89,7 @@ class TerrainTemplate : SWGParser { globalWaterTableShaderSize = chunk.readFloat() globalWaterTableShaderTemplateName = chunk.readString() environmentCycleTime = chunk.readFloat() - + if (form.version == 13) { chunk.readString() chunk.readString() @@ -78,7 +103,7 @@ class TerrainTemplate : SWGParser { chunk.readInt() chunk.readString() } - + collidableMinimumDistance = chunk.readFloat() collidableMaximumDistance = chunk.readFloat() collidableTileSize = chunk.readFloat() @@ -99,16 +124,16 @@ class TerrainTemplate : SWGParser { farRadialTileSize = chunk.readFloat() farRadialTileBorder = chunk.readFloat() farRadialSeed = chunk.readInt() - + legacyMap = if (form.version >= 15) chunk.readBoolean() else true } - + form.readForm("TGEN").use { tgen -> tgen.readForm("MGRP")?.use { fractalGroup.read(it) } tgen.readForm("MGRP")?.use { bitmapGroup.read(it) } - + // Load layers - tgen.readForm("LYRS").use { lyrs -> + tgen.readForm("LYRS").use { lyrs -> lyrs.readAllForms("LAYR") { layer -> layer.use { val terrainListLayer = TerrainListLayer() @@ -119,7 +144,7 @@ class TerrainTemplate : SWGParser { } } } - + override fun write(): IffForm { val data = IffChunk("DATA") data.writeString(name) @@ -131,7 +156,7 @@ class TerrainTemplate : SWGParser { data.writeFloat(globalWaterTableShaderSize) data.writeString(globalWaterTableShaderTemplateName) data.writeFloat(environmentCycleTime) - + data.writeFloat(collidableMinimumDistance) data.writeFloat(collidableMaximumDistance) data.writeFloat(collidableTileSize) @@ -152,74 +177,70 @@ class TerrainTemplate : SWGParser { data.writeFloat(farRadialTileSize) data.writeFloat(farRadialTileBorder) data.writeInt(farRadialSeed) - - if (!legacyMap) - data.writeBoolean(false) // Only use the new v15 format if it's not legacy - + + if (!legacyMap) data.writeBoolean(false) // Only use the new v15 format if it's not legacy + val version = if (legacyMap) 14 else 15 - + val children = ArrayList() children.add(fractalGroup.write()) children.add(bitmapGroup.write()) - val layerChildren = ArrayList(); + val layerChildren = ArrayList() for (child in topTerrainLayer.children) { layerChildren.add(child.write()) } - children.add(IffForm.of("LYRS", layerChildren)); - + children.add(IffForm.of("LYRS", layerChildren)) + val terrainGeneratorForm = IffForm.of("TGEN", 0, children) - + return IffForm.of("PTAT", version, data, terrainGeneratorForm) } - + fun isBitmapReferenced(bitmapId: Int): Boolean { return topTerrainLayer.isBitmapReferenced(bitmapId) } - + fun getHeight(x: Float, z: Float): TerrainInformation { val waterHeight = getWaterHeight(x, z) val terrainHeight = getTerrainHeight(x, z) - - if (waterHeight.isNaN() || terrainHeight.height >= waterHeight) - return terrainHeight - + + if (waterHeight.isNaN() || terrainHeight.height >= waterHeight) return terrainHeight + return TerrainInformation(waterHeight, 0f, 1f, 0f) } - + fun getWaterHeight(x: Float, z: Float): Float { var height = -Float.MAX_VALUE - if (useGlobalWaterTable) - height = globalWaterTableHeight + if (useGlobalWaterTable) height = globalWaterTableHeight height = max(height, getWaterHeightRecursive(Point2f(x, z), topTerrainLayer)) - + return if (height == -Float.MAX_VALUE) Float.NaN else height } - + fun isWater(x: Float, z: Float): Boolean { val waterHeight = getWaterHeight(x, z) - - if (waterHeight.isNaN()) - return false - + + if (waterHeight.isNaN()) return false + return getTerrainHeight(x, z).height <= waterHeight } - + fun getTerrainHeight(x: Float, z: Float): TerrainInformation { // can be cached - val tileSize = chunkWidth / (2*numberOfTilesPerChunk) + val tileSize = chunkWidth / (2 * numberOfTilesPerChunk) val halfMap = mapWidth / 2 // smart stuff val tileLocalX = ((x + halfMap) % chunkWidth) / tileSize val tileLocalZ = ((z + halfMap) % chunkWidth) / tileSize val tileX = x - (tileLocalX - tileLocalX.toInt()) * tileSize val tileZ = z - (tileLocalZ - tileLocalZ.toInt()) * tileSize - + val angleRight = ((tileLocalX.toInt() xor tileLocalZ.toInt()) and 1) == 0 val sideLeft = when (angleRight) { false -> ((tileLocalX - tileLocalX.toInt()) <= 1 - (tileLocalZ - tileLocalZ.toInt())) // left - true -> ((tileLocalX - tileLocalX.toInt()) <= (tileLocalZ - tileLocalZ.toInt())) // top + true -> ((tileLocalX - tileLocalX.toInt()) <= (tileLocalZ - tileLocalZ.toInt())) // top } - + val planeInfo = FloatArray(9) planeInfo[0] = tileX planeInfo[2] = tileZ @@ -240,84 +261,48 @@ class TerrainTemplate : SWGParser { } } // Set the y value for each vertex - planeInfo[1] = getHeightAt(planeInfo[0], planeInfo[2]).height - planeInfo[4] = getHeightAt(planeInfo[3], planeInfo[5]).height - planeInfo[7] = getHeightAt(planeInfo[6], planeInfo[8]).height - - // Setup the a and b vectors for the cross product + planeInfo[1] = getHeightAt(planeInfo[0], planeInfo[2]) + planeInfo[4] = getHeightAt(planeInfo[3], planeInfo[5]) + planeInfo[7] = getHeightAt(planeInfo[6], planeInfo[8]) + + // Set up the a and b vectors for the cross product for (i in 0..2) { - planeInfo[3+i] -= planeInfo[i] - planeInfo[6+i] -= planeInfo[i] + planeInfo[3 + i] -= planeInfo[i] + planeInfo[6 + i] -= planeInfo[i] } - + // Cross product! This is the plane normal val cx = planeInfo[4] * planeInfo[8] - planeInfo[5] * planeInfo[7] val cy = planeInfo[5] * planeInfo[6] - planeInfo[3] * planeInfo[8] val cz = planeInfo[3] * planeInfo[7] - planeInfo[4] * planeInfo[6] val planeOffset = planeInfo[0] * cx + planeInfo[1] * cy + planeInfo[2] * cz val height = (planeOffset - cx * x - cz * z) / cy - + return TerrainInformation(height, cx, cy, cz) } - - private fun getHeightAt(x: Float, z: Float): HeightInformation { + + private fun getHeightAt(x: Float, z: Float): Float { val p = Point2f(x, z) - val height = HeightInformation(1f, 0f) - getLayerHeight(topTerrainLayer, p, height) - return height + return getLayerHeight(topTerrainLayer, p, 1f, 0f) } - - private fun getLayerHeight(layer: TerrainListLayer, p: Point2f, height: HeightInformation) { - var transformValue = if (layer.boundaries.isEmpty()) 1f else 0f - - val rectangle = layer.extent - for (boundary in layer.boundaries) { - if (!boundary.isContained(p)) - continue - - transformValue = max(transformValue, calculateFeathering(boundary.process(p), boundary.featherType)) - } - - if (layer.invertBoundaries) - transformValue = 1.0f - transformValue - - for (filter in layer.filters) { - transformValue = min(transformValue, calculateFeathering(filter.process(p, transformValue, height.height, rectangle, lookupInformation), filter.featherType)) - if (transformValue == 0f) - break - } - - assert(transformValue in 0f..1f) - - if (layer.invertFilters) - transformValue = 1.0f - transformValue - + + private fun getLayerHeight(layer: TerrainListLayer, p: Point2f, previousTransformValue: Float, previousHeight: Float): Float { + val transformValue = calculateTransformValue(lookupInformation, layer, p, previousHeight) + + var newHeight = previousHeight if (transformValue > 0f) { for (affector in layer.heights) { - height.height = affector.process(p, transformValue * height.transformValue, height.height, lookupInformation) + newHeight = affector.process(p, transformValue * previousTransformValue, newHeight, lookupInformation) } } - - val onlySubLayers = layer.boundaries.isEmpty() && layer.filters.isEmpty() && layer.heights.isEmpty() - if (onlySubLayers) - transformValue = 1f - val returnHeight = HeightInformation(transformValue * height.transformValue, height.height) + + val newTransformValue = transformValue * previousTransformValue for (child in layer.children) { - getLayerHeight(child, p, returnHeight) + newHeight = getLayerHeight(child, p, newTransformValue, newHeight) } - height.height = returnHeight.height + return newHeight } - - private fun calculateFeathering(value: Float, featheringType: Int): Float { - return when (featheringType) { - 0 -> value - 1 -> value * value - 2 -> sqrt(value) - 3 -> value * value * (3 - 2 * value) - else -> 0f - } - } - + private fun getWaterHeightRecursive(p: Point2f, layer: TerrainListLayer): Float { var waterHeight = -Float.MAX_VALUE for (boundary in layer.boundaries) { @@ -332,8 +317,47 @@ class TerrainTemplate : SWGParser { } return waterHeight } - + data class TerrainInformation(val height: Float, val normalX: Float, val normalY: Float, val normalZ: Float) - private class HeightInformation(var transformValue: Float, var height: Float) - + + companion object { + + private fun calculateTransformValue(lookupInformation: TerrainInfoLookup, layer: TerrainListLayer, p: Point2f, height: Float): Float { + var transformValue = if (layer.boundaries.isEmpty()) 1f else 0f + + val rectangle = layer.extent + for (boundary in layer.boundaries) { + if (!boundary.isContained(p)) continue + + transformValue = max(transformValue, calculateFeathering(boundary.process(p), boundary.featherType)) + } + + if (layer.invertBoundaries) transformValue = 1.0f - transformValue + + for (filter in layer.filters) { + transformValue = min(transformValue, calculateFeathering(filter.process(p, transformValue, height, rectangle, lookupInformation), filter.featherType)) + if (transformValue == 0f) break + } + + if (layer.invertFilters) transformValue = 1.0f - transformValue + + val onlySubLayers = layer.boundaries.isEmpty() && layer.filters.isEmpty() && layer.heights.isEmpty() + if (onlySubLayers) transformValue = 1f + + assert(transformValue in 0f..1f) + return transformValue + } + + private fun calculateFeathering(value: Float, featheringType: Int): Float { + return when (featheringType) { + 0 -> value + 1 -> value * value + 2 -> sqrt(value) + 3 -> value * value * (3 - 2 * value) + else -> 0f + } + } + + } + }