diff --git a/.gitignore b/.gitignore index 6d3553cff..3a29e1488 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ classes/ .classpath .project .kotlin +/.idea/AndroidProjectSystem.xml # IntelliJ-User Specific .idea/**/workspace.xml .idea/**/tasks.xml diff --git a/pswgcommon b/pswgcommon index fdad8e7ad..6dfcf2213 160000 --- a/pswgcommon +++ b/pswgcommon @@ -1 +1 @@ -Subproject commit fdad8e7ad724403827508f53ad2542b819cc0d0a +Subproject commit 6dfcf2213bffccd3b202d9315f258bf947587c38 diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NavigationPoint.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NavigationPoint.kt index 123adc1ec..a41c6d9d5 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NavigationPoint.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NavigationPoint.kt @@ -120,9 +120,13 @@ class NavigationPoint(val parent: SWGObject?, val location: Location, val speed: } fun from(sourceParent: SWGObject?, source: Location, destinationParent: SWGObject?, destination: Location, speed: Double): List { + assert(sourceParent == null || sourceParent is CellObject) { "invalid source parent" } + assert(destinationParent == null || destinationParent is CellObject) { "invalid destination parent" } + assert(speed > 0) { "speed must be greater than zero, was $speed" } + + if (sourceParent == destinationParent) return from(sourceParent, source, destination, speed) + var source = source - assert(sourceParent == null || sourceParent is CellObject) - assert(destinationParent == null || destinationParent is CellObject) val route = getBuildingRoute(sourceParent as CellObject?, destinationParent as CellObject?, source, destination) ?: return ArrayList() val points = createIntraBuildingRoute(route, sourceParent, source, speed) if (route.isNotEmpty()) source = if (destinationParent == null) buildWorldPortalLocation(route[route.size - 1]) else buildPortalLocation(route[route.size - 1]) @@ -145,10 +149,14 @@ class NavigationPoint(val parent: SWGObject?, val location: Location, val speed: val totalDistance = source.distanceTo(destination) val path: MutableList = ArrayList() + assert(speed > 0) { "speed must be greater than zero, was $speed" } + assert(totalDistance < 5_000) { "distance between waypoints is too large ($totalDistance)" } + var currentDistance = speed while (currentDistance < totalDistance) { path.add(interpolate(parent, source, destination, speed, currentDistance / totalDistance)) currentDistance += speed + assert(path.size < 10_000) { "path length growing too large" } } path.add(interpolate(parent, source, destination, speed, 1.0)) return path @@ -229,7 +237,7 @@ class NavigationPoint(val parent: SWGObject?, val location: Location, val speed: private fun buildWorldPortalLocation(portal: Portal): Location { val building = portal.cell1!!.parent - assert(building is BuildingObject) + assert(building is BuildingObject) { "cell parent wasn't a building" } return Location.builder(buildPortalLocation(portal)).translateLocation(building!!.location).build() } diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcPatrolMode.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcPatrolMode.kt index e996a7f3e..c39909927 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcPatrolMode.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcPatrolMode.kt @@ -51,15 +51,19 @@ class NpcPatrolMode(obj: AIObject, waypoints: List) : Np waypointBuilder.add(waypointBuilder[0]) } - this.waypoints = ArrayList(128) - for (i in 1 until waypointBuilder.size) { - val source = waypointBuilder[i - 1] - val destination = waypointBuilder[i] - this.waypoints.addAll(NavigationPoint.from(source.parent, source.location, destination.parent, destination.location, walkSpeed)) - if (destination.delay > 0) - this.waypoints.addAll(NavigationPoint.nop(this.waypoints[this.waypoints.size - 1], destination.delay.toInt() - 1)) + if (waypointBuilder.isEmpty()) { + this.waypoints = ArrayList(128) + } else { + this.waypoints = ArrayList(128) + for (i in 1 until waypointBuilder.size) { + val source = waypointBuilder[i - 1] + val destination = waypointBuilder[i] + this.waypoints.addAll(NavigationPoint.from(source.parent, source.location, destination.parent, destination.location, walkSpeed)) + if (destination.delay > 0) + this.waypoints.addAll(NavigationPoint.nop(this.waypoints[this.waypoints.size - 1], destination.delay.toInt() - 1)) + } + this.waypoints.addAll(NavigationPoint.from(waypointBuilder[waypointBuilder.size - 1].parent, waypointBuilder[waypointBuilder.size - 1].location, waypointBuilder[0].parent, waypointBuilder[0].location, walkSpeed)) } - this.waypoints.addAll(NavigationPoint.from(waypointBuilder[waypointBuilder.size - 1].parent, waypointBuilder[waypointBuilder.size - 1].location, waypointBuilder[0].parent, waypointBuilder[0].location, walkSpeed)) } override suspend fun onModeStart() { diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt index 81b1ed58c..764dcae71 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt @@ -39,13 +39,16 @@ import com.projectswg.holocore.resources.support.objects.permissions.AdminPermis import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty import com.projectswg.holocore.resources.support.objects.swg.custom.AIBehavior import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject -import java.util.concurrent.ThreadLocalRandom import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min import kotlin.math.sin +import kotlin.random.Random class DynamicMovementObject(var location: Location, val name: String, val baseSpeed: Double = 0.0) { - - var heading = ThreadLocalRandom.current().nextDouble() * 2 * Math.PI + + private val random = Random(System.currentTimeMillis()) + var heading = random.nextDouble() * 2 * Math.PI private val groupMarker = ObjectCreator.createObjectFromTemplate("object/path_waypoint/shared_path_waypoint_droid.iff") private val npcs = ArrayList() private var lastUpdate = System.nanoTime() @@ -70,7 +73,6 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp val bossSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.BOSS).build(), groupMarker) val eliteSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.ELITE).build(), groupMarker) val normalSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.NORMAL).build(), groupMarker) - val random = ThreadLocalRandom.current() if (random.nextDouble() < 0.25) npcs.add(NPCCreator.createSingleNpc(bossSpawner)) npcs.add(NPCCreator.createSingleNpc(eliteSpawner)) @@ -102,7 +104,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp .setZ(location.z + radius * sin(angle)) newLocationBuilder.setY(ServerData.terrains.getHeight(newLocationBuilder)) val newLocation = newLocationBuilder.build() - val speed = it.worldLocation.distanceTo(newLocation) / elapsedTime + val speed = min(30.0, max(1.0, it.worldLocation.distanceTo(newLocation) / elapsedTime)) it.moveTo(null, newLocationBuilder.build(), speed) } } @@ -114,7 +116,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp return } - val newHeading = heading + Math.PI * (1 + ThreadLocalRandom.current().nextDouble() - 0.5) + val newHeading = heading + Math.PI * (1 + random.nextDouble() - 0.5) val secondProposed = calculateNextPosition(newHeading, distance) if (isValidNextPosition(secondProposed)) { location = secondProposed @@ -123,7 +125,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp } // Brute Force Escape - val randomRotationFromNorth = ThreadLocalRandom.current().nextDouble() * Math.TAU + val randomRotationFromNorth = random.nextDouble() * Math.TAU for (clockwiseRotation in 0..35) { val bruteForceHeading = (clockwiseRotation * 10) * Math.PI / 180.0 + randomRotationFromNorth val proposed = calculateNextPosition(bruteForceHeading, distance) @@ -135,6 +137,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp } // TODO: destroy this object, we got stuck + location = firstProposed assert(false) } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java index 1c8bcd0c5..2f4f14ce7 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java @@ -796,17 +796,62 @@ public abstract class SWGObject extends BaselineObject implements Comparable. * + ***********************************************************************************/ +package com.projectswg.holocore.resources.support.data.server_info.loader + +import com.projectswg.common.data.location.Location +import com.projectswg.common.data.swgiff.parsers.SWGParser +import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcPatrolRouteLoader +import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint +import com.projectswg.holocore.resources.support.npc.spawn.Spawner +import com.projectswg.holocore.services.support.objects.ObjectStorageService +import com.projectswg.holocore.test.runners.TestRunnerNoIntents +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class NpcPatrolRouteLoaderTest : TestRunnerNoIntents() { + + @Test + fun `test patrol route waypoints`() { + fun checkRouteLeg(sourceWaypoint: Spawner.ResolvedPatrolWaypoint, destinationWaypoint: Spawner.ResolvedPatrolWaypoint) { + val route = NavigationPoint.from(sourceWaypoint.parent, sourceWaypoint.location, destinationWaypoint.parent, destinationWaypoint.location, 1.0) + assert(route.size < 500) { "distance between waypoints is too large (${route.size})" } + assert(sourceWaypoint.location.terrain == destinationWaypoint.location.terrain) { "terrain mismatch along route" } + } + + val hasError = AtomicBoolean(false) + ServerData.npcPatrolRoutes.forEach { route -> + try { + val resolvedRoute = route.map { Spawner.ResolvedPatrolWaypoint(it) } + assert(route.isNotEmpty()) { "route is empty" } + for (i in 1 until route.size) { + checkRouteLeg(resolvedRoute[i - 1], resolvedRoute[i]) + } + if (route[0].patrolType == NpcPatrolRouteLoader.PatrolType.LOOP) checkRouteLeg(resolvedRoute[route.size - 1], resolvedRoute[0]) + } catch (e: AssertionError) { + System.err.println("Patrol group '${route[0].groupId}' error: ${e.message}") + hasError.set(true) + } + } + Assertions.assertFalse(hasError.get()) + } + + @Test + fun `test NPC to patrol route start`() { + val hasError = AtomicBoolean(false) + ServerData.npcStaticSpawns.spawns.parallelStream().forEach { spawn -> + if (spawn.patrolId.isEmpty() || spawn.patrolId == "0") return@forEach + val spawnerLocation = Location.builder().setTerrain(spawn.terrain).setX(spawn.x).setY(spawn.y).setZ(spawn.z).build() + val route = ServerData.npcPatrolRoutes[spawn.patrolId] + val routeLocation = Location.builder().setTerrain(route[0].terrain).setX(route[0].x).setY(route[0].y).setZ(route[0].z).build() + val distanceToRoute = spawnerLocation.distanceTo(routeLocation) + try { + assert(spawn.buildingId == route[0].buildingId) { "NPC not in same building as route" } + assert(spawn.cellId == route[0].cellId) { "NPC not in same cell as route" } + assert(distanceToRoute < 500) { "Spawner distance to route too large ($distanceToRoute)" } + assert(spawnerLocation.terrain == routeLocation.terrain) { "terrain mismatch along route" } + } catch (e: AssertionError) { + System.err.println("Patrol spawner '${spawn.npcId}' with route '${spawn.patrolId}' error: ${e.message}") + hasError.set(true) + } + } + Assertions.assertFalse(hasError.get()) + } + + companion object { + + private var objectStorageService = ObjectStorageService() + + @BeforeAll + @JvmStatic + fun setup() { + SWGParser.setBasePath("serverdata") + objectStorageService.initialize() + } + + @AfterAll + @JvmStatic + fun tearDown() { + objectStorageService.terminate() + } + } + +} \ No newline at end of file