From 826b24a7af9aa1e71b112328f123217a8a113464 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sun, 19 Jan 2025 00:57:58 -0600 Subject: [PATCH 1/6] Added initial version of the dynamic spawn system --- pswgcommon | 2 +- .../server_info/loader/NoSpawnZoneLoader.kt | 11 +-- .../npc/ai/dynamic/DynamicMovementObject.kt | 92 +++++++++++++++++++ .../ai/dynamic/DynamicMovementProcessor.kt | 52 +++++++++++ .../npc/ai/AIDynamicMovementService.kt | 34 +++++++ .../services/support/npc/ai/AIManager.kt | 4 +- .../npc/ai/dynamic/TestDynamicMovement.kt | 55 +++++++++++ .../dynamic/TestDynamicMovementProcessor.kt | 56 +++++++++++ 8 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt create mode 100644 src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt create mode 100644 src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt create mode 100644 src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt create mode 100644 src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovementProcessor.kt diff --git a/pswgcommon b/pswgcommon index dbc640f31..4d5fde9ac 160000 --- a/pswgcommon +++ b/pswgcommon @@ -1 +1 @@ -Subproject commit dbc640f314fb70e0092ac5619115e85ed5a2d271 +Subproject commit 4d5fde9ac80cc74cce0c1c882394b1aca837e433 diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/NoSpawnZoneLoader.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/NoSpawnZoneLoader.kt index ab8fc92f6..964a5462e 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/NoSpawnZoneLoader.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/NoSpawnZoneLoader.kt @@ -1,11 +1,10 @@ /*********************************************************************************** - * Copyright (c) 2024 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * + * 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 Holocore. * * * @@ -33,7 +32,6 @@ import com.projectswg.holocore.resources.support.data.server_info.SdbLoader.SdbR import java.io.File import java.io.IOException import java.util.* -import kotlin.collections.ArrayList class NoSpawnZoneLoader : DataLoader() { private val noSpawnZoneMap: MutableMap> = EnumMap(Terrain::class.java) @@ -89,6 +87,7 @@ class NoSpawnZoneLoader : DataLoader() { } class NoSpawnZoneInfo(set: SdbResultSet) { + val name: String = set.getText("comment") val x: Long = set.getInt("x") val z: Long = set.getInt("z") val radius: Long = set.getInt("radius") 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 new file mode 100644 index 000000000..f4a33dfcd --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt @@ -0,0 +1,92 @@ +/*********************************************************************************** + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore 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. * + * * + * Holocore 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 Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.resources.support.npc.ai.dynamic + +import com.projectswg.common.data.location.Location +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import java.util.concurrent.ThreadLocalRandom +import kotlin.math.cos +import kotlin.math.sin + +class DynamicMovementObject(var location: Location) { + + var heading = ThreadLocalRandom.current().nextDouble() * 2 * Math.PI + + fun move() { + assert(!isOutOfBounds(location)) + assert(heading in 0.0..Math.TAU) + moveNextPosition() + } + + private fun moveNextPosition() { + val firstProposed = calculateNextPosition(heading) + if (isValidNextPosition(firstProposed)) { + location = firstProposed + return + } + + val newHeading = heading + Math.PI * (1 + ThreadLocalRandom.current().nextDouble() - 0.5) + val secondProposed = calculateNextPosition(newHeading) + if (isValidNextPosition(secondProposed)) { + location = secondProposed + heading = if (newHeading >= Math.TAU) newHeading - Math.TAU else newHeading + return + } + + // Brute Force Escape + val randomRotationFromNorth = ThreadLocalRandom.current().nextDouble() * Math.TAU + for (clockwiseRotation in 0..35) { + val bruteForceHeading = (clockwiseRotation * 10) * Math.PI / 180.0 + randomRotationFromNorth + val proposed = calculateNextPosition(bruteForceHeading) + if (isValidNextPosition(proposed)) { + location = proposed + heading = if (bruteForceHeading >= Math.TAU) bruteForceHeading - Math.TAU else bruteForceHeading + return + } + } + + // TODO: destroy this object, we got stuck + assert(false) + } + + private fun isValidNextPosition(proposed: Location): Boolean { + return !DynamicMovementProcessor.isIntersectingProtectedZone(location, proposed) && !isOutOfBounds(proposed) + } + + private fun isOutOfBounds(location: Location): Boolean { + return location.x < -7000 || location.x > 7000 || location.z < -7000 || location.z > 7000 + } + + private fun calculateNextPosition(heading: Double): Location { + val nextLocationBuilder = Location.builder(location) + .setX(location.x + 50 * cos(heading)) + .setZ(location.z + 50 * sin(heading)) + .setHeading(heading) + nextLocationBuilder.setY(ServerData.terrains.getHeight(nextLocationBuilder)) + return nextLocationBuilder.build() + } + +} \ No newline at end of file diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt new file mode 100644 index 000000000..320f03881 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt @@ -0,0 +1,52 @@ +/*********************************************************************************** + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore 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. * + * * + * Holocore 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 Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.resources.support.npc.ai.dynamic + +import com.projectswg.common.data.location.Location +import com.projectswg.common.data.location.Point2f +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import kotlin.math.sqrt + +object DynamicMovementProcessor { + + fun isIntersectingProtectedZone(start: Location, end: Location): Boolean { + assert(start.terrain == end.terrain) + val zones = ServerData.noSpawnZones.getNoSpawnZoneInfos(start.terrain) + val startPoint = Point2f(start.x.toFloat(), start.z.toFloat()) + val endPoint = Point2f(end.x.toFloat(), end.z.toFloat()) + + for (zone in zones) { + val zoneCenter = Point2f(zone.x.toFloat(), zone.z.toFloat()) + val closestPoint = startPoint.getClosestPointOnLineSegment(endPoint, zoneCenter) + val distance = sqrt(closestPoint.squaredDistanceTo(zoneCenter)) + if (distance < zone.radius.toFloat()) { + return true + } + } + return false + } + +} \ No newline at end of file diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt new file mode 100644 index 000000000..31d681b7d --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt @@ -0,0 +1,34 @@ +/*********************************************************************************** + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore 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. * + * * + * Holocore 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 Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.services.support.npc.ai + +import me.joshlarson.jlcommon.control.Service + +class AIDynamicMovementService : Service() { + + + +} \ No newline at end of file diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt index 4a49f423f..784bce757 100644 --- a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt +++ b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt @@ -1,5 +1,5 @@ /*********************************************************************************** - * Copyright (c) 2024 /// Project SWG /// www.projectswg.com * + * 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. * @@ -28,5 +28,5 @@ package com.projectswg.holocore.services.support.npc.ai import me.joshlarson.jlcommon.control.Manager import me.joshlarson.jlcommon.control.ManagerStructure -@ManagerStructure(children = [AIService::class, AIMovementService::class]) +@ManagerStructure(children = [AIService::class, AIDynamicMovementService::class, AIMovementService::class]) class AIManager : Manager() diff --git a/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt b/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt new file mode 100644 index 000000000..5e169abda --- /dev/null +++ b/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt @@ -0,0 +1,55 @@ +/*********************************************************************************** + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore 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. * + * * + * Holocore 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 Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.resources.support.npc.ai.dynamic + +import com.mongodb.assertions.Assertions +import com.projectswg.common.data.location.Location +import com.projectswg.common.data.location.Terrain +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import com.projectswg.holocore.test.runners.TestRunnerNoIntents +import org.junit.jupiter.api.Test + +class TestDynamicMovement : TestRunnerNoIntents() { + + @Test + fun testWanderNoCollide() { + val obj = DynamicMovementObject(Location.builder() + .setTerrain(Terrain.TATOOINE) + .setX(1024.toDouble()) + .setY(ServerData.terrains.getHeight(Terrain.TATOOINE, 1024.toDouble(), 1024.toDouble())) + .setZ(1024.toDouble()) + .build()) + + var previousLocation = obj.location + for (i in 0 until 1000) { + obj.move() + val newLocation = obj.location + Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(previousLocation, newLocation)) + previousLocation = newLocation + } + } + +} diff --git a/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovementProcessor.kt b/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovementProcessor.kt new file mode 100644 index 000000000..5c5ed4eb9 --- /dev/null +++ b/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovementProcessor.kt @@ -0,0 +1,56 @@ +/*********************************************************************************** + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore 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. * + * * + * Holocore 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 Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.resources.support.npc.ai.dynamic + +import com.mongodb.assertions.Assertions +import com.projectswg.common.data.location.Location +import com.projectswg.common.data.location.Terrain +import com.projectswg.holocore.test.runners.TestRunnerNoIntents +import org.junit.jupiter.api.Test + +class TestDynamicMovementProcessor : TestRunnerNoIntents() { + + @Test + fun testNoBuildZoneCheck() { + // No intersections + Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(-559, -1197))) + Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(5710, -5478))) + Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(4433, -4013))) + Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(3577, -1067), tatooine(-2141, 4485))) + Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(3577, -1067), tatooine(5812, 5580))) + + // Intersections + Assertions.assertTrue(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(5812, 5580))) + Assertions.assertTrue(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(3505, -4811))) + Assertions.assertTrue(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(2503, -5798))) + Assertions.assertTrue(DynamicMovementProcessor.isIntersectingProtectedZone(tatooine(5391, -3185), tatooine(-5246, -3984))) + } + + private fun tatooine(x: Int, z: Int): Location { + return Location.builder().setTerrain(Terrain.TATOOINE).setX(x.toDouble()).setZ(z.toDouble()).build() + } + +} From cae344edcb298f1f8cb26770010ae7aa89ac3130 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Tue, 21 Jan 2025 01:59:38 -0600 Subject: [PATCH 2/6] Added NPC spawning to dynamic movement objects --- .../npc/ai/dynamic/DynamicMovementObject.kt | 86 ++++++++++++++++--- .../ai/dynamic/DynamicMovementProcessor.kt | 30 +++++++ .../support/objects/swg/custom/AIObject.kt | 19 +++- .../npc/ai/AIDynamicMovementService.kt | 72 ++++++++++++++++ .../npc/ai/dynamic/TestDynamicMovement.kt | 6 +- 5 files changed, 197 insertions(+), 16 deletions(-) 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 f4a33dfcd..81b1ed58c 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 @@ -26,30 +26,96 @@ package com.projectswg.holocore.resources.support.npc.ai.dynamic import com.projectswg.common.data.location.Location +import com.projectswg.holocore.intents.support.objects.DestroyObjectIntent +import com.projectswg.holocore.intents.support.objects.ObjectCreatedIntent import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader +import com.projectswg.holocore.resources.support.npc.spawn.NPCCreator +import com.projectswg.holocore.resources.support.npc.spawn.SimpleSpawnInfo +import com.projectswg.holocore.resources.support.npc.spawn.Spawner +import com.projectswg.holocore.resources.support.npc.spawn.SpawnerType +import com.projectswg.holocore.resources.support.objects.ObjectCreator +import com.projectswg.holocore.resources.support.objects.permissions.AdminPermissions +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.sin -class DynamicMovementObject(var location: Location) { +class DynamicMovementObject(var location: Location, val name: String, val baseSpeed: Double = 0.0) { var heading = ThreadLocalRandom.current().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() - fun move() { + init { + groupMarker.location = location + groupMarker.objectName = name + groupMarker.containerPermissions = AdminPermissions.getPermissions() + } + + fun launch() { + ObjectCreatedIntent(groupMarker).broadcast() + val simpleSpawnInfo = SimpleSpawnInfo.builder() + .withNpcId("humanoid_tusken_soldier") + .withDifficulty(CreatureDifficulty.NORMAL) + .withSpawnerType(SpawnerType.WAYPOINT_AUTO_SPAWN) + .withMinLevel(10) + .withMaxLevel(30) + .withSpawnerFlag(NpcStaticSpawnLoader.SpawnerFlag.AGGRESSIVE) + .withBehavior(AIBehavior.IDLE) + .withLocation(location) + 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)) + repeat(15) { + npcs.add(NPCCreator.createSingleNpc(normalSpawner)) + } + } + + fun destroy() { + DestroyObjectIntent(groupMarker).broadcast() + } + + fun act() { assert(!isOutOfBounds(location)) assert(heading in 0.0..Math.TAU) - moveNextPosition() + val currentTime = System.nanoTime() + val npcSpeed = if (baseSpeed != 0.0) baseSpeed else npcs[0].walkSpeed.toDouble() + val elapsedTime = (currentTime - lastUpdate) / 1E9 + val distance = elapsedTime * npcSpeed // TODO: scale based on terrain, somewhat + lastUpdate = currentTime + moveNextPosition(distance) + groupMarker.moveToLocation(location) + npcs.forEachIndexed { i, it -> + // Spiral shape for now + val radius = 1 + i * 0.5 + val angle = i * (Math.PI * 0.61) + val newLocationBuilder = Location.builder(location) + .setX(location.x + radius * cos(angle)) + .setZ(location.z + radius * sin(angle)) + newLocationBuilder.setY(ServerData.terrains.getHeight(newLocationBuilder)) + val newLocation = newLocationBuilder.build() + val speed = it.worldLocation.distanceTo(newLocation) / elapsedTime + it.moveTo(null, newLocationBuilder.build(), speed) + } } - private fun moveNextPosition() { - val firstProposed = calculateNextPosition(heading) + private fun moveNextPosition(distance: Double) { + val firstProposed = calculateNextPosition(heading, distance) if (isValidNextPosition(firstProposed)) { location = firstProposed return } val newHeading = heading + Math.PI * (1 + ThreadLocalRandom.current().nextDouble() - 0.5) - val secondProposed = calculateNextPosition(newHeading) + val secondProposed = calculateNextPosition(newHeading, distance) if (isValidNextPosition(secondProposed)) { location = secondProposed heading = if (newHeading >= Math.TAU) newHeading - Math.TAU else newHeading @@ -60,7 +126,7 @@ class DynamicMovementObject(var location: Location) { val randomRotationFromNorth = ThreadLocalRandom.current().nextDouble() * Math.TAU for (clockwiseRotation in 0..35) { val bruteForceHeading = (clockwiseRotation * 10) * Math.PI / 180.0 + randomRotationFromNorth - val proposed = calculateNextPosition(bruteForceHeading) + val proposed = calculateNextPosition(bruteForceHeading, distance) if (isValidNextPosition(proposed)) { location = proposed heading = if (bruteForceHeading >= Math.TAU) bruteForceHeading - Math.TAU else bruteForceHeading @@ -80,10 +146,10 @@ class DynamicMovementObject(var location: Location) { return location.x < -7000 || location.x > 7000 || location.z < -7000 || location.z > 7000 } - private fun calculateNextPosition(heading: Double): Location { + private fun calculateNextPosition(heading: Double, distance: Double): Location { val nextLocationBuilder = Location.builder(location) - .setX(location.x + 50 * cos(heading)) - .setZ(location.z + 50 * sin(heading)) + .setX(location.x + distance * cos(heading)) + .setZ(location.z + distance * sin(heading)) .setHeading(heading) nextLocationBuilder.setY(ServerData.terrains.getHeight(nextLocationBuilder)) return nextLocationBuilder.build() diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt index 320f03881..ad242b6d9 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt @@ -27,11 +27,41 @@ package com.projectswg.holocore.resources.support.npc.ai.dynamic import com.projectswg.common.data.location.Location import com.projectswg.common.data.location.Point2f +import com.projectswg.common.data.location.Terrain import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import java.util.concurrent.ThreadLocalRandom import kotlin.math.sqrt object DynamicMovementProcessor { + fun createSpawnLocation(terrain: Terrain): Location? { + val randomizer = ThreadLocalRandom.current() + repeat(100) { + val locationBuilder = Location.builder() + .setTerrain(terrain) + .setX(randomizer.nextDouble() * 7000 * 2 - 7000) + .setZ(randomizer.nextDouble() * 7000 * 2 - 7000) + locationBuilder.setY(ServerData.terrains.getHeight(locationBuilder)) + val location = locationBuilder.build() + if (isGoodSpawnLocation(location)) + return location + } + return null + } + + fun isGoodSpawnLocation(point: Location): Boolean { + val zones = ServerData.noSpawnZones.getNoSpawnZoneInfos(point.terrain) + val point2f = Point2f(point.x.toFloat(), point.z.toFloat()) + for (zone in zones) { + val zoneCenter = Point2f(zone.x.toFloat(), zone.z.toFloat()) + val distance = sqrt(point2f.squaredDistanceTo(zoneCenter)) + if (distance < zone.radius.toFloat()) { + return false + } + } + return true + } + fun isIntersectingProtectedZone(start: Location, end: Location): Boolean { assert(start.terrain == end.terrain) val zones = ServerData.noSpawnZones.getNoSpawnZoneInfos(start.terrain) diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt index 0e8fc7c44..a754d8cba 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt @@ -26,9 +26,11 @@ package com.projectswg.holocore.resources.support.objects.swg.custom import com.projectswg.common.data.encodables.oob.StringId +import com.projectswg.common.data.location.Location import com.projectswg.common.network.packets.swg.zone.baselines.Baseline.BaselineType import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText import com.projectswg.holocore.resources.support.color.SWGColor.Reds.red +import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint import com.projectswg.holocore.resources.support.npc.ai.NpcCombatMode import com.projectswg.holocore.resources.support.npc.ai.NpcIdleMode import com.projectswg.holocore.resources.support.npc.spawn.Spawner @@ -39,12 +41,11 @@ import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureSt import com.projectswg.holocore.resources.support.objects.swg.tangible.OptionFlag import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject import com.projectswg.holocore.utilities.cancelAndWait -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.* import java.time.Instant import java.util.* import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.atomic.AtomicReference class AIObject(objectId: Long) : CreatureObject(objectId) { private val playersNearby: MutableSet = CopyOnWriteArraySet() @@ -55,6 +56,7 @@ class AIObject(objectId: Long) : CreatureObject(objectId) { private var coroutineScope: CoroutineScope? = null private var previousScheduled: Job? = null private var questionMarkBlockedUntil: Instant = Instant.now() + private var movementJob = AtomicReference(null) val defaultWeapons: List get() { @@ -205,6 +207,17 @@ class AIObject(objectId: Long) : CreatureObject(objectId) { } } + fun moveTo(newParent: SWGObject?, location: Location, speed: Double) { + val newMovementTask = coroutineScope?.launch { + val route = NavigationPoint.from(this@AIObject.parent, this@AIObject.location, newParent, location, speed) + for (point in route) { + point.move(this@AIObject) + delay(1000L) + } + } ?: return + movementJob.getAndSet(newMovementTask)?.cancel() + } + fun start(coroutineScope: CoroutineScope) { this.coroutineScope = CoroutineScope(coroutineScope.coroutineContext + SupervisorJob()) startMode(activeMode ?: defaultMode) diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt index 31d681b7d..fe3a76c95 100644 --- a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt +++ b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt @@ -25,10 +25,82 @@ ***********************************************************************************/ package com.projectswg.holocore.services.support.npc.ai +import com.projectswg.common.data.location.Location +import com.projectswg.common.data.location.Terrain +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase.config +import com.projectswg.holocore.resources.support.npc.ai.dynamic.DynamicMovementObject +import com.projectswg.holocore.resources.support.npc.ai.dynamic.DynamicMovementProcessor +import com.projectswg.holocore.utilities.HolocoreCoroutine +import com.projectswg.holocore.utilities.cancelAndWait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import me.joshlarson.jlcommon.control.Service +import me.joshlarson.jlcommon.log.Log +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList class AIDynamicMovementService : Service() { + + private val coroutineScope = HolocoreCoroutine.childScope() + private val dynamicObjects = EnumMap>(Terrain::class.java) + init { + // Guarantee all terrains are set so that we don't have to worry about concurrent access later + for (terrain in Terrain.entries) { + dynamicObjects[terrain] = CopyOnWriteArrayList() + } + } + override fun start(): Boolean { + val spawnsPerPlanet = config.getInt(this, "spawnsPerPlanet", 10) + for (terrain in listOf(Terrain.TATOOINE)) { + launchDynamicMovementObjectHandler(terrain, "DynamicObject-${terrain.name}-dev") + repeat(spawnsPerPlanet) { + launchDynamicMovementObjectHandler(terrain, "DynamicObject-${terrain.name}-$it") + } + } + return super.start() + } + + override fun stop(): Boolean { + coroutineScope.cancelAndWait() + return super.stop() + } + private fun launchDynamicMovementObjectHandler(terrain: Terrain, objectName: String) { + coroutineScope.launch { + try { + while (isActive) { + delay(5_000L) + val spawnLocation = if (objectName.endsWith("-dev")) + Location.builder().setTerrain(terrain).setX(1024.0).setZ(1024.0).setY(ServerData.terrains.getHeight(terrain, 1024.0, 1024.0)).build() + else + DynamicMovementProcessor.createSpawnLocation(terrain) ?: continue + Log.d("Created dynamic movement object: %s", spawnLocation) + val dynamicObject = DynamicMovementObject(spawnLocation, objectName) + dynamicObject.launch() + dynamicObjects[terrain]!!.add(dynamicObject) + try { + handleObjectLoop(dynamicObject) + } finally { + dynamicObject.destroy() + dynamicObjects[terrain]!!.remove(dynamicObject) + } + } + } finally { + + } + } + } + + private suspend fun CoroutineScope.handleObjectLoop(dynamicObject: DynamicMovementObject) { + while (isActive) { + dynamicObject.act() + delay(10_000L) + } + } + } \ No newline at end of file diff --git a/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt b/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt index 5e169abda..d296c4ec9 100644 --- a/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt +++ b/src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt @@ -41,11 +41,11 @@ class TestDynamicMovement : TestRunnerNoIntents() { .setX(1024.toDouble()) .setY(ServerData.terrains.getHeight(Terrain.TATOOINE, 1024.toDouble(), 1024.toDouble())) .setZ(1024.toDouble()) - .build()) + .build(), "devtest", 100.0) var previousLocation = obj.location - for (i in 0 until 1000) { - obj.move() + repeat(1000) { + obj.act() val newLocation = obj.location Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(previousLocation, newLocation)) previousLocation = newLocation From 8beb4b6c30c2fad261b5092038c15f5e76bbaff0 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sun, 18 May 2025 19:50:19 -0500 Subject: [PATCH 3/6] Bumped versions --- build.gradle.kts | 18 +++++++++--------- pswgcommon | 2 +- .../mongodb/PswgUserDatabaseMongoTest.kt | 13 +++++++------ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a07e844db..8471f4a6e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,13 +33,13 @@ plugins { application idea java - kotlin("jvm") version "2.1.0" + kotlin("jvm") version "2.1.21" id("org.beryx.jlink") version "3.1.1" } val javaVersion = JavaVersion.current() val kotlinTargetJdk = JvmTarget.fromTarget(javaVersion.majorVersion) -val junit5Version = "5.11.3" +val junit5Version = "5.12.2" val holocoreLogLevel: String? by project subprojects { @@ -77,23 +77,23 @@ dependencies { implementation(project(":pswgcommon")) implementation(kotlin("stdlib")) implementation(kotlin("reflect")) - implementation(group="org.jetbrains.kotlinx", name="kotlinx-coroutines-core", version="1.9.0") - implementation(group="org.mongodb", name="mongodb-driver-sync", version="5.2.1") + implementation(group="org.jetbrains.kotlinx", name="kotlinx-coroutines-core", version="1.10.2") + implementation(group="org.mongodb", name="mongodb-driver-sync", version="5.5.0") implementation(group="me.joshlarson", name="fast-json", version="3.0.1") implementation(group="me.joshlarson", name="jlcommon-network", version="1.1.0") implementation(group="me.joshlarson", name="jlcommon-argparse", version="0.9.6") implementation(group="me.joshlarson", name="websocket", version="0.9.4") val slf4jVersion = "1.7.36" - runtimeOnly(group="org.slf4j", name="slf4j-jdk14", version= slf4jVersion) + runtimeOnly(group="org.slf4j", name="slf4j-jdk14", version=slf4jVersion) utilityImplementation(project(":")) utilityImplementation(project(":pswgcommon")) - + testImplementation(group="org.junit.jupiter", name="junit-jupiter-api", version=junit5Version) testRuntimeOnly(group="org.junit.jupiter", name="junit-jupiter-engine", version=junit5Version) - testRuntimeOnly(group="org.junit.platform", name="junit-platform-launcher", version="1.11.3") + testRuntimeOnly(group="org.junit.platform", name="junit-platform-launcher", version="1.12.2") testImplementation(group="org.junit.jupiter", name="junit-jupiter-params", version=junit5Version) - testImplementation(group="org.testcontainers", name="mongodb", version="1.20.4") + testImplementation(group="org.testcontainers", name="mongodb", version="1.21.0") testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0") } @@ -193,7 +193,7 @@ tasks.register("createRunScript") { val modulePath = "$runtimeClasspath${File.pathSeparator}$mainJavaOutputDir" // Assemble the command - val command = "clear; JAVA_HOME=$javaHome ./gradlew classes && $javaExecutable -ea -p $modulePath -m holocore/com.projectswg.holocore.ProjectSWG --print-colors" + val command = "clear; JAVA_HOME=$javaHome ./gradlew classes && $javaExecutable -Xms1G -Xmx2G -XX:+UseZGC -XX:+ZGenerational -ea -p $modulePath -m holocore/com.projectswg.holocore.ProjectSWG --print-colors" // File to write the run command val outputFile = file("${layout.buildDirectory.asFile.get().absolutePath}/run") diff --git a/pswgcommon b/pswgcommon index 4d5fde9ac..fdad8e7ad 160000 --- a/pswgcommon +++ b/pswgcommon @@ -1 +1 @@ -Subproject commit 4d5fde9ac80cc74cce0c1c882394b1aca837e433 +Subproject commit fdad8e7ad724403827508f53ad2542b819cc0d0a diff --git a/src/test/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabaseMongoTest.kt b/src/test/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabaseMongoTest.kt index ca2d2b560..c0ba2631e 100644 --- a/src/test/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabaseMongoTest.kt +++ b/src/test/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabaseMongoTest.kt @@ -1,11 +1,10 @@ /*********************************************************************************** - * Copyright (c) 2023 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * + * 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 Holocore. * * * @@ -29,8 +28,10 @@ package com.projectswg.holocore.resources.support.data.server_info.mongodb import com.mongodb.client.MongoDatabase import com.projectswg.holocore.resources.support.data.server_info.database.PswgUserDatabase import org.bson.Document -import org.junit.jupiter.api.* +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test class PswgUserDatabaseMongoTest { From 6ebe34ad19ffb5b839164332707809a5c24b71a9 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sun, 18 May 2025 19:53:42 -0500 Subject: [PATCH 4/6] Optimized CPU/memory usage --- .../intents/support/npc/ai/AiIntents.kt | 40 ----- .../support/npc/ai/NavigationPoint.kt | 11 +- .../resources/support/npc/ai/NpcCombatMode.kt | 3 +- .../resources/support/npc/ai/NpcLoiterMode.kt | 6 +- .../resources/support/npc/ai/NpcPatrolMode.kt | 62 ++++--- .../support/npc/ai/NpcTurningMode.kt | 12 +- .../objects/awareness/ObjectAware.java | 22 ++- .../objects/awareness/TerrainMapChunk.kt | 25 ++- .../support/objects/swg/SWGObject.java | 38 ++++- .../objects/swg/creature/CreatureObject.java | 5 +- .../support/objects/swg/custom/AIObject.kt | 26 ++- .../support/objects/swg/custom/NpcMode.kt | 21 +-- .../support/data/PacketRecordingService.kt | 12 +- .../services/support/npc/ai/AIManager.kt | 2 +- .../support/npc/ai/AIMovementService.kt | 160 ------------------ .../support/npc/spawn/SpawnerService.kt | 5 +- .../objects/awareness/AwarenessService.java | 2 +- .../support/npc/ai/TestAIMovementService.kt | 65 ------- 18 files changed, 147 insertions(+), 370 deletions(-) delete mode 100644 src/main/java/com/projectswg/holocore/intents/support/npc/ai/AiIntents.kt delete mode 100644 src/main/java/com/projectswg/holocore/services/support/npc/ai/AIMovementService.kt delete mode 100644 src/test/java/com/projectswg/holocore/services/support/npc/ai/TestAIMovementService.kt diff --git a/src/main/java/com/projectswg/holocore/intents/support/npc/ai/AiIntents.kt b/src/main/java/com/projectswg/holocore/intents/support/npc/ai/AiIntents.kt deleted file mode 100644 index 7259359fb..000000000 --- a/src/main/java/com/projectswg/holocore/intents/support/npc/ai/AiIntents.kt +++ /dev/null @@ -1,40 +0,0 @@ -/*********************************************************************************** - * Copyright (c) 2024 /// 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 Holocore. * - * * - * --------------------------------------------------------------------------------* - * * - * Holocore 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. * - * * - * Holocore 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 Holocore. If not, see . * - ***********************************************************************************/ -package com.projectswg.holocore.intents.support.npc.ai - -import com.projectswg.common.data.location.Location -import com.projectswg.holocore.resources.support.npc.ai.NavigationOffset -import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint -import com.projectswg.holocore.resources.support.npc.ai.NavigationRouteType -import com.projectswg.holocore.resources.support.objects.swg.SWGObject -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject -import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject -import com.projectswg.holocore.resources.support.objects.swg.custom.NpcMode -import me.joshlarson.jlcommon.control.Intent - -data class CompileNpcMovementIntent(val obj: AIObject, val points: List, val type: NavigationRouteType, val speed: Double, val offset: NavigationOffset? = null) : Intent() -data class StartNpcMovementIntent(val obj: AIObject, val parent: SWGObject?, val destination: Location, val speed: Double) : Intent() -data class StopNpcMovementIntent(val obj: AIObject) : Intent() 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 29e0d61c2..123adc1ec 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 @@ -1,11 +1,10 @@ /*********************************************************************************** - * Copyright (c) 2024 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * + * 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 Holocore. * * * @@ -37,7 +36,7 @@ import java.util.* import kotlin.math.atan2 import kotlin.math.floor -class NavigationPoint private constructor(val parent: SWGObject?, val location: Location, val speed: Double) { +class NavigationPoint(val parent: SWGObject?, val location: Location, val speed: Double) { private val hash = Objects.hash(parent, location) fun move(obj: SWGObject) { diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt index 7cf768e90..c93d963ce 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt @@ -30,7 +30,6 @@ import com.projectswg.common.data.encodables.tangible.Posture import com.projectswg.common.data.location.Location import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText import com.projectswg.holocore.intents.support.global.command.QueueCommandIntent -import com.projectswg.holocore.intents.support.npc.ai.StopNpcMovementIntent import com.projectswg.holocore.intents.support.objects.MoveObjectIntent import com.projectswg.holocore.resources.support.color.SWGColor import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData @@ -80,7 +79,7 @@ class NpcCombatMode(obj: AIObject, coroutineScope: CoroutineScope) : NpcMode(obj override suspend fun onModeStart() { showExclamationMarkAboveNpc() - StopNpcMovementIntent(ai).broadcast() + ai.stopMovement() returnLocation.set(NavigationPoint.at(ai.parent, ai.location, npcRunSpeed)) startCombatLocation.set(ai.worldLocation) } diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcLoiterMode.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcLoiterMode.kt index d8daa312c..b7c56d8ee 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcLoiterMode.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcLoiterMode.kt @@ -44,8 +44,10 @@ class NpcLoiterMode(obj: AIObject, private val radius: Double) : NpcMode(obj) { override suspend fun onModeStart() { val currentLocation = ai.location - if (mainLocation == null) mainLocation = currentLocation - if (mainLocation!!.distanceTo(currentLocation) >= 1) runTo(mainLocation) + var startingLocation = mainLocation + if (startingLocation == null) startingLocation = currentLocation + if (startingLocation.distanceTo(currentLocation) >= 1) runTo(startingLocation) + mainLocation = startingLocation } override suspend fun onModeLoop() { 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 2d0bb943b..e996a7f3e 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 @@ -25,7 +25,7 @@ ***********************************************************************************/ package com.projectswg.holocore.resources.support.npc.ai -import com.projectswg.holocore.intents.support.npc.ai.CompileNpcMovementIntent +import com.projectswg.common.data.location.Location import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcPatrolRouteLoader.PatrolType import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader import com.projectswg.holocore.resources.support.npc.spawn.Spawner.ResolvedPatrolWaypoint @@ -51,15 +51,28 @@ class NpcPatrolMode(obj: AIObject, waypoints: List) : Np waypointBuilder.add(waypointBuilder[0]) } - this.waypoints = ArrayList(waypointBuilder.size) - for (waypoint in waypointBuilder) { - val point = NavigationPoint.at(waypoint.parent, waypoint.location, walkSpeed) - this.waypoints.add(point) - this.waypoints.addAll(NavigationPoint.nop(point, waypoint.delay.toInt())) + 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)) } override suspend fun onModeStart() { + val route = calculateRouteOffset(calculateInitialRoutePoints()) + + ai.moveVia(route, loop = true) + } + + override suspend fun onModeLoop() { + throw CancellationException() // No loop necessary + } + + private fun calculateInitialRoutePoints(): List { val compiledWaypoints: MutableList if (waypoints.isNotEmpty()) { var index = 0 @@ -67,7 +80,7 @@ class NpcPatrolMode(obj: AIObject, waypoints: List) : Np for (i in 1 until waypoints.size) { if (waypoints[i].isNoOperation) continue - + val distance = waypoints[i].distanceTo(ai) if (distance < closestDistance) { closestDistance = distance @@ -88,46 +101,49 @@ class NpcPatrolMode(obj: AIObject, waypoints: List) : Np } else { compiledWaypoints = waypoints } + return compiledWaypoints + } + + private fun calculateRouteOffset(compiledWaypoints: List): List { val spawner = spawner ?: throw CancellationException() val spacing = 3.0 val position = spawner.npcs.indexOf(ai) + var offsetX = 0.0 + var offsetZ = 0.0 assert(position != -1) when (spawner.patrolFormation) { - NpcStaticSpawnLoader.PatrolFormation.NONE -> CompileNpcMovementIntent(ai, compiledWaypoints, NavigationRouteType.LOOP, walkSpeed, null).broadcast() + NpcStaticSpawnLoader.PatrolFormation.NONE -> {} NpcStaticSpawnLoader.PatrolFormation.COLUMN -> { - val x = if (position % 2 == 0) 0.0 else spacing - val z = -(spacing * ceil((position - 1) / 2.0)) - CompileNpcMovementIntent(ai, compiledWaypoints, NavigationRouteType.LOOP, walkSpeed, NavigationOffset(x, z)).broadcast() + offsetX = if (position % 2 == 0) 0.0 else spacing + offsetZ = -(spacing * ceil((position - 1) / 2.0)) } NpcStaticSpawnLoader.PatrolFormation.WEDGE -> { - val x = spacing * ceil(position / 2.0) - val z = -x - CompileNpcMovementIntent(ai, compiledWaypoints, NavigationRouteType.LOOP, walkSpeed, NavigationOffset(if (position % 2 == 0) -x else x, z)).broadcast() + offsetX = spacing * ceil(position / 2.0) * (if (position % 2 == 0) -1 else 1) + offsetZ = -offsetX } NpcStaticSpawnLoader.PatrolFormation.LINE -> { - val x = spacing * ceil(position / 2.0) - CompileNpcMovementIntent(ai, compiledWaypoints, NavigationRouteType.LOOP, walkSpeed, NavigationOffset(if (position % 2 == 0) -x else x, 0.0)).broadcast() + offsetX = spacing * ceil(position / 2.0) * (if (position % 2 == 0) -1 else 1) } NpcStaticSpawnLoader.PatrolFormation.BOX -> { - val x = when (position) { + offsetX = when (position) { 0, 1, 2 -> position * 3.0 3 -> 0.0 4 -> 6.0 else -> (position - 5) * 3.0 } - val z = when (position) { + offsetZ = when (position) { 0, 1, 2 -> 0.0 // front of the box 3, 4 -> 3.0 // sides of the box else -> 6.0 // back of the box } - CompileNpcMovementIntent(ai, compiledWaypoints, NavigationRouteType.LOOP, walkSpeed, NavigationOffset(x, z)).broadcast() } } - } - - override suspend fun onModeLoop() { - throw CancellationException() // No loop necessary + + val offsetWaypoints = ArrayList(compiledWaypoints.size) + for (wp in compiledWaypoints) + offsetWaypoints.add(NavigationPoint(wp.parent, Location.builder(wp.location).translatePosition(offsetX, 0.0, offsetZ).build(), wp.speed)) + return offsetWaypoints } } diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcTurningMode.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcTurningMode.kt index d6f450e06..501746ad4 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcTurningMode.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcTurningMode.kt @@ -42,13 +42,11 @@ class NpcTurningMode(obj: AIObject) : NpcMode(obj) { override suspend fun onModeStart() { val currentLocation = ai.location - if (mainLocation == null) - mainLocation = currentLocation - if (mainParent == null) - mainParent = ai.parent - - if (mainLocation!!.distanceTo(currentLocation) >= 1) - runTo(mainLocation) + var startingLocation = mainLocation + if (startingLocation == null) startingLocation = currentLocation + if (mainParent == null) mainParent = ai.parent + if (startingLocation.distanceTo(currentLocation) >= 1) runTo(startingLocation) + mainLocation = startingLocation } override suspend fun onModeLoop() { diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/ObjectAware.java b/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/ObjectAware.java index e7d0b4b1b..111c3714c 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/ObjectAware.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/ObjectAware.java @@ -1,11 +1,10 @@ /*********************************************************************************** - * Copyright (c) 2018 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * + * 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 Holocore. * * * @@ -32,6 +31,7 @@ import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -56,7 +56,9 @@ public synchronized void setAware(@NotNull AwarenessType type, @NotNull Collecti @NotNull public Set getAware() { - return getAwareStream().collect(Collectors.toSet()); + Set aware = new HashSet<>(); + forEachAware(aware::add); + return aware; } @NotNull @@ -77,8 +79,12 @@ protected TerrainMapChunk getTerrainMapChunk() { return chunk.get(); } - private Stream getAwareStream() { - return awareness.values().stream().flatMap(Collection::stream); + public void forEachAware(Consumer handler) { + for (Collection objects : awareness.values()) { + for (SWGObject object : objects) { + handler.accept(object); + } + } } private boolean notAware(SWGObject test) { diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.kt b/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.kt index 816409bc1..bc7c1882d 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.kt @@ -1,29 +1,28 @@ /*********************************************************************************** - * Copyright (c) 2018 /// Project SWG /// www.projectswg.com * - * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * - * * + * 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 Holocore. * - * * + * * * --------------------------------------------------------------------------------* - * * + * * * Holocore 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. * - * * + * * * Holocore 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 Holocore. If not, see //www.gnu.org/licenses/>. * - */ + * along with Holocore. If not, see . * + ***********************************************************************************/ package com.projectswg.holocore.resources.support.objects.awareness import com.projectswg.holocore.resources.support.objects.swg.SWGObject 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 7763fe427..1c8bcd0c5 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 @@ -1,11 +1,10 @@ /*********************************************************************************** - * Copyright (c) 2024 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * + * 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 Holocore. * * * @@ -437,6 +436,15 @@ public boolean isLineOfSight(@NotNull SWGObject target) { public SWGObject getSlottedObject(String slotName) { return slots.get(slotName); } + + /** + * Determines whether an object is stored in the specified slot. + * @param slotName The slot name to look up. + * @return TRUE if the slot has an object in it, FALSE otherwise. + */ + public boolean isSlotPopulated(String slotName) { + return slots.containsKey(slotName); + } public Collection getChildObjects() { Set ret = new HashSet<>(containedObjects.size() + slots.size()); @@ -520,8 +528,10 @@ public Collection getSlottedObjects() { public void setLocation(Location location) { if (parent != null && location.getTerrain() != parent.getTerrain()) throw new IllegalArgumentException("Attempted to set different terrain from parent!"); + Terrain previousTerrain = this.location.getTerrain(); this.location.setLocation(location); - updateChildrenTerrain(); + if (previousTerrain != location.getTerrain()) + updateChildrenTerrain(); } public void setTerrain(@NotNull Terrain terrain) { @@ -536,8 +546,10 @@ public void setTerrain(@NotNull Terrain terrain) { public void setPosition(@NotNull Terrain terrain, double x, double y, double z) { if (parent != null && terrain != parent.getTerrain()) throw new IllegalArgumentException("Attempted to set different terrain from parent!"); + Terrain previousTerrain = this.location.getTerrain(); location.setPosition(terrain, x, y, z); - updateChildrenTerrain(); + if (previousTerrain != location.getTerrain()) + updateChildrenTerrain(); } public void setPosition(double x, double y, double z) { @@ -1060,13 +1072,13 @@ public void onObjectExitedAware(SWGObject aware) { public void onObjectMoved() { if (!isGenerated()) return; - for (SWGObject a : getAware()) { + forEachAware(a -> { try { a.onObjectMoveInAware(this); } catch (Throwable t) { Log.e(t); } - } + }); } /** @@ -1081,6 +1093,10 @@ public Set getObserverCreatures() { return Collections.unmodifiableSet(observers); } + public int getObserverCreatureCount() { + return observers.size(); + } + public Set getObservers() { return observers.stream().map(CreatureObject::getOwnerShallow).filter(Objects::nonNull).collect(Collectors.toSet()); } @@ -1097,6 +1113,10 @@ public Set getAware() { return awareness.getAware(); } + public void forEachAware(Consumer handler) { + awareness.forEachAware(handler); + } + public Set getAware(AwarenessType type) { return awareness.getAware(type); } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObject.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObject.java index 3cbdc43c7..dd1b64932 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObject.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObject.java @@ -373,7 +373,7 @@ public PlayerObject getPlayerObject() { } public boolean isPlayer() { - return getSlottedObject("ghost") != null; + return isSlotPopulated("ghost"); } public SWGObject getMissionBag() { @@ -403,8 +403,7 @@ public void setPosture(Posture posture) { public void setRace(Race race) { this.race = race; } - - + public void inheritMovement(CreatureObject vehicle) { setWalkSpeed(vehicle.getRunSpeed() / 2); setRunSpeed(vehicle.getRunSpeed()); diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt index a754d8cba..5be59913b 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt @@ -207,17 +207,27 @@ class AIObject(objectId: Long) : CreatureObject(objectId) { } } - fun moveTo(newParent: SWGObject?, location: Location, speed: Double) { - val newMovementTask = coroutineScope?.launch { - val route = NavigationPoint.from(this@AIObject.parent, this@AIObject.location, newParent, location, speed) - for (point in route) { - point.move(this@AIObject) - delay(1000L) - } - } ?: return + fun moveTo(newParent: SWGObject?, location: Location, speed: Double, loop: Boolean = false) { + moveVia(NavigationPoint.from(this@AIObject.parent, this@AIObject.location, newParent, location, speed), loop) + } + + fun moveVia(route: List, loop: Boolean) { + val newMovementTask = (coroutineScope ?: return).launch { + if (route.isEmpty()) return@launch + do { + for (point in route) { + point.move(this@AIObject) + delay(1000L) + } + } while (loop) + } movementJob.getAndSet(newMovementTask)?.cancel() } + fun stopMovement() { + movementJob.getAndSet(null)?.cancel() + } + fun start(coroutineScope: CoroutineScope) { this.coroutineScope = CoroutineScope(coroutineScope.coroutineContext + SupervisorJob()) startMode(activeMode ?: defaultMode) diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.kt b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.kt index 8aa70fb79..0317e5a2e 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.kt @@ -1,5 +1,5 @@ /*********************************************************************************** - * Copyright (c) 2024 /// Project SWG /// www.projectswg.com * + * 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. * @@ -27,10 +27,7 @@ package com.projectswg.holocore.resources.support.objects.swg.custom import com.projectswg.common.data.encodables.tangible.Posture import com.projectswg.common.data.location.Location -import com.projectswg.holocore.intents.support.npc.ai.CompileNpcMovementIntent import com.projectswg.holocore.intents.support.objects.MoveObjectIntent -import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint -import com.projectswg.holocore.resources.support.npc.ai.NavigationRouteType import com.projectswg.holocore.resources.support.npc.spawn.Spawner import com.projectswg.holocore.resources.support.objects.swg.SWGObject import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject @@ -106,19 +103,19 @@ abstract class NpcMode(val ai: AIObject) { MoveObjectIntent(ai, location!!, walkSpeed).broadcast() } - fun walkTo(parent: SWGObject?, location: Location?) { - CompileNpcMovementIntent(ai, NavigationPoint.from(ai.parent, ai.location, parent, location!!, walkSpeed), NavigationRouteType.TERMINATE, walkSpeed, null).broadcast() + fun walkTo(parent: SWGObject?, location: Location) { + ai.moveTo(parent, location, walkSpeed) } - fun walkTo(location: Location?) { - CompileNpcMovementIntent(ai, NavigationPoint.from(ai.parent, ai.location, location!!, walkSpeed), NavigationRouteType.TERMINATE, walkSpeed, null).broadcast() + fun walkTo(location: Location) { + ai.moveTo(null, location, walkSpeed) } - fun runTo(parent: SWGObject?, location: Location?) { - CompileNpcMovementIntent(ai, NavigationPoint.from(ai.parent, ai.location, parent, location!!, runSpeed), NavigationRouteType.TERMINATE, runSpeed, null).broadcast() + fun runTo(parent: SWGObject?, location: Location) { + ai.moveTo(parent, location, runSpeed) } - fun runTo(location: Location?) { - CompileNpcMovementIntent(ai, NavigationPoint.from(ai.parent, ai.location, location!!, runSpeed), NavigationRouteType.TERMINATE, runSpeed, null).broadcast() + fun runTo(location: Location) { + ai.moveTo(null, location, runSpeed) } } diff --git a/src/main/java/com/projectswg/holocore/services/support/data/PacketRecordingService.kt b/src/main/java/com/projectswg/holocore/services/support/data/PacketRecordingService.kt index fcc199dc5..c25421613 100644 --- a/src/main/java/com/projectswg/holocore/services/support/data/PacketRecordingService.kt +++ b/src/main/java/com/projectswg/holocore/services/support/data/PacketRecordingService.kt @@ -1,11 +1,10 @@ /*********************************************************************************** - * Copyright (c) 2023 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2025 /// Project SWG /// www.projectswg.com * * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * 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 an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * + * 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 Holocore. * * * @@ -53,6 +52,5 @@ class PacketRecordingService : Service() { packetLogger.log("%s %d:\t%s", if (`in`) "IN " else "OUT", networkId, str) } - private val packetDebug: Boolean - get() = config.getBoolean(this, "packetLogging", false) + private val packetDebug = config.getBoolean(this, "packetLogging", false) } diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt index 784bce757..2e03e863e 100644 --- a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt +++ b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIManager.kt @@ -28,5 +28,5 @@ package com.projectswg.holocore.services.support.npc.ai import me.joshlarson.jlcommon.control.Manager import me.joshlarson.jlcommon.control.ManagerStructure -@ManagerStructure(children = [AIService::class, AIDynamicMovementService::class, AIMovementService::class]) +@ManagerStructure(children = [AIService::class, AIDynamicMovementService::class]) class AIManager : Manager() diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIMovementService.kt b/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIMovementService.kt deleted file mode 100644 index 23d3fa65b..000000000 --- a/src/main/java/com/projectswg/holocore/services/support/npc/ai/AIMovementService.kt +++ /dev/null @@ -1,160 +0,0 @@ -/*********************************************************************************** - * 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 Holocore. * - * * - * --------------------------------------------------------------------------------* - * * - * Holocore 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. * - * * - * Holocore 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 Holocore. If not, see . * - ***********************************************************************************/ -package com.projectswg.holocore.services.support.npc.ai - -import com.projectswg.common.data.location.Location -import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent -import com.projectswg.holocore.intents.support.npc.ai.CompileNpcMovementIntent -import com.projectswg.holocore.intents.support.npc.ai.StartNpcMovementIntent -import com.projectswg.holocore.intents.support.npc.ai.StopNpcMovementIntent -import com.projectswg.holocore.resources.support.npc.ai.NavigationOffset -import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint -import com.projectswg.holocore.resources.support.npc.ai.NavigationRouteType -import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject -import com.projectswg.holocore.utilities.HolocoreCoroutine -import com.projectswg.holocore.utilities.cancelAndWait -import com.projectswg.holocore.utilities.launchWithFixedRate -import me.joshlarson.jlcommon.control.IntentHandler -import me.joshlarson.jlcommon.control.Service -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import kotlin.math.cos -import kotlin.math.sin - -class AIMovementService : Service() { - - private val routes = ConcurrentHashMap() - private val coroutineScope = HolocoreCoroutine.childScope() - - override fun start(): Boolean { - coroutineScope.launchWithFixedRate(1, TimeUnit.SECONDS) { - routes.values.forEach { it.execute() } // TODO: put each route in its own coroutine - } - return true - } - - override fun stop(): Boolean { - coroutineScope.cancelAndWait() - return super.stop() - } - - @IntentHandler - private fun handleStartNpcMovementIntent(snmi: StartNpcMovementIntent) { - val obj = snmi.obj - - val route = NavigationPoint.from(obj.parent, obj.location, snmi.parent, snmi.destination, snmi.speed) - if (route.isEmpty()) - routes.remove(obj) - else - routes[obj] = NavigationRoute(obj, route, NavigationRouteType.TERMINATE) - } - - @IntentHandler - private fun handleCompileNpcMovementIntent(snmi: CompileNpcMovementIntent) { - val obj = snmi.obj - val route = ArrayList(snmi.points.size) - val waypoints = snmi.points - for ((index, point) in waypoints.withIndex()) { - val next = waypoints.getOrNull(index+1) ?: waypoints.getOrNull(0) - appendRoutePoint(route, offsetLocation(point, if (next == null) 0.0 else point.location.getHeadingTo(next.location), snmi.offset), snmi.speed) - } - - if (route.isEmpty()) - routes.remove(obj) - else - routes[obj] = NavigationRoute(obj, route, snmi.type) - } - - @IntentHandler - private fun handleStopNpcMovementIntent(snmi: StopNpcMovementIntent) { - routes.remove(snmi.obj) - } - - @IntentHandler - private fun handleCreatureKilledIntent(cki: CreatureKilledIntent) { - val corpse = cki.corpse - if (corpse is AIObject) - routes.remove(corpse) - } - - private fun appendRoutePoint(waypoints: MutableList, waypoint: NavigationPoint, speed: Double) { - val prev = if (waypoints.isEmpty()) null else waypoints[waypoints.size - 1] - if (waypoint.isNoOperation) { - waypoints.add(waypoint) - return - } - if (prev == null) { - waypoints.add(NavigationPoint.at(waypoint.parent, waypoint.location, speed)) - } else { - if (prev.location.equals(waypoint.location) && prev.parent === waypoint.parent) - return - waypoints.addAll(NavigationPoint.from(prev.parent, prev.location, waypoint.parent, waypoint.location, speed)) - } - } - - private class NavigationRoute(private val obj: AIObject, private val route: List, private val type: NavigationRouteType) { - - private val index = AtomicInteger(0) - - fun execute() { - var index = this.index.getAndIncrement() - if (index >= route.size) { - when (type) { - NavigationRouteType.LOOP -> { - this.index.set(0) - index = 0 - } - NavigationRouteType.TERMINATE -> { - StopNpcMovementIntent(obj).broadcast() - return - } - } - } - assert(index < route.size && index >= 0) - - route[index].move(obj) - } - } - - companion object { - - internal fun offsetLocation(point: NavigationPoint, heading: Double, offset: NavigationOffset?): NavigationPoint { - return if (offset == null) point else NavigationPoint.at(point.parent, offsetLocation(point.location, heading, offset), point.speed) - } - - internal fun offsetLocation(location: Location, heading: Double, offset: NavigationOffset): Location { - val oX = offset.x - val oZ = offset.z - val cos = cos(Math.toRadians(heading)) // heading should be 0 - 360, with 0 representing north and 270 representing east - val sin = sin(Math.toRadians(heading)) - val nX = oX * cos - oZ * sin - val nZ = oZ * cos + oX * sin - return Location.builder(location).setX(location.x + nX).setZ(location.z + nZ).build() - } - } - -} diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.kt b/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.kt index 208ce5d02..a7baabf0d 100644 --- a/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.kt +++ b/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.kt @@ -1,5 +1,5 @@ /*********************************************************************************** - * Copyright (c) 2024 /// Project SWG /// www.projectswg.com * + * 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. * @@ -174,14 +174,13 @@ class SpawnerService : Service() { val startTime = StandardLog.onStartLoad("spawners") var count = 0 - for (spawn in ServerData.npcStaticSpawns.spawns) { + ServerData.npcStaticSpawns.spawns.parallelStream().forEach { spawn -> try { spawn(spawn) count++ } catch (t: Throwable) { Log.e("Failed to load spawner[%s]/npc[%s]. %s: %s", spawn.id, spawn.npcId, t.javaClass.name, t.message) } - } StandardLog.onEndLoad(count, "spawners", startTime) diff --git a/src/main/java/com/projectswg/holocore/services/support/objects/awareness/AwarenessService.java b/src/main/java/com/projectswg/holocore/services/support/objects/awareness/AwarenessService.java index 68338cb52..d0d035539 100644 --- a/src/main/java/com/projectswg/holocore/services/support/objects/awareness/AwarenessService.java +++ b/src/main/java/com/projectswg/holocore/services/support/objects/awareness/AwarenessService.java @@ -232,7 +232,7 @@ private void sendObjectUpdates(@NotNull SWGObject obj, @Nullable SWGObject oldPa private static void onObjectMoved(@NotNull SWGObject obj, @Nullable SWGObject oldParent, @Nullable SWGObject newParent, @NotNull Location oldLocation, @NotNull Location newLocation, double speed) { if (obj instanceof CreatureObject && ((CreatureObject) obj).isLoggedInPlayer()) new PlayerTransformedIntent((CreatureObject) obj, oldParent, newParent, oldLocation, newLocation).broadcast(); - else if (obj.getObserverCreatures().isEmpty()) + else if (obj.getObserverCreatureCount() == 0) return; // If a tree falls in the forest and nobody is there to hear it... if (newParent != null) { diff --git a/src/test/java/com/projectswg/holocore/services/support/npc/ai/TestAIMovementService.kt b/src/test/java/com/projectswg/holocore/services/support/npc/ai/TestAIMovementService.kt deleted file mode 100644 index 83c355f10..000000000 --- a/src/test/java/com/projectswg/holocore/services/support/npc/ai/TestAIMovementService.kt +++ /dev/null @@ -1,65 +0,0 @@ -/*********************************************************************************** - * Copyright (c) 2019 /// Project SWG /// www.projectswg.com * - * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * - * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * - * Our goal is to create an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * - * * - * This file is part of Holocore. * - * * - * --------------------------------------------------------------------------------* - * * - * Holocore 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. * - * * - * Holocore 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 Holocore. If not, see . * - ***********************************************************************************/ - -package com.projectswg.holocore.services.support.npc.ai - -import com.projectswg.common.data.location.Location -import com.projectswg.holocore.resources.support.npc.ai.NavigationOffset -import com.projectswg.holocore.test.runners.TestRunnerNoIntents -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import kotlin.math.sqrt - -class TestAIMovementService: TestRunnerNoIntents() { - - @Test - fun testOffsetLocation() { - assertEquals(0.0, headingTo(0.0, 1.0), 1E-7, "NORTH") - assertEquals(270.0, headingTo(1.0, 0.0), 1E-7, "EAST") - assertEquals(180.0, headingTo(0.0, -1.0), 1E-7, "SOUTH") - assertEquals(90.0, headingTo(-1.0, 0.0), 1E-7, "WEST") - - testOffset(headingTo(0.0, 1.0), 1.0, 1.0) - testOffset(headingTo(-1.0, 0.0), -1.0, 1.0) - testOffset(headingTo(0.0, -1.0), -1.0, -1.0) - testOffset(headingTo(1.0, 0.0), 1.0, -1.0) - - testOffset(headingTo(1.0, 1.0), sqrt(2.0), 0.0) - } - - private fun headingTo(eX: Double, eZ: Double): Double { - return Location.builder().setPosition(0.0, 0.0, 0.0).build().getHeadingTo(Location.builder().setPosition(eX, 0.0, eZ).build()) - } - - private fun testOffset(heading: Double, tx: Double, tz: Double) { - val startLocation = Location.builder().setPosition(0.0, 0.0, 0.0).build() - val endLocation = AIMovementService.offsetLocation(startLocation, heading, NavigationOffset(1.0, 1.0)) - assertEquals(tx, endLocation.x, 1E-7, "X") - assertEquals(tz, endLocation.z, 1E-7, "Z") - } - -} From 5107f27c5a6e9e4abf3cbee39f4fc1424a0b96ba Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sun, 18 May 2025 19:54:16 -0500 Subject: [PATCH 5/6] Bumped version of kotlinc in .idea --- .idea/kotlinc.xml | 2 +- .idea/misc.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index bb4493707..1e16934f6 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e73a28f20..bb3e7433f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - From e7496db079381afc1805a2e9c76ad4df45cd8947 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sun, 25 May 2025 15:35:48 -0500 Subject: [PATCH 6/6] Fixed or improved a variety of CPU and RAM issues --- .gitignore | 1 + pswgcommon | 2 +- .../support/npc/ai/NavigationPoint.kt | 14 ++- .../resources/support/npc/ai/NpcPatrolMode.kt | 20 ++-- .../npc/ai/dynamic/DynamicMovementObject.kt | 17 +-- .../support/objects/swg/SWGObject.java | 61 ++++++++-- .../loader/NpcPatrolRouteLoaderTest.kt | 108 ++++++++++++++++++ 7 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 src/test/java/com/projectswg/holocore/resources/support/data/server_info/loader/NpcPatrolRouteLoaderTest.kt 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 Location getLocation() { public Location getWorldLocation() { return location.getWorldLocation(this); } - + public double distanceTo(@NotNull SWGObject obj) { - if (parent == obj.getParent()) - return getLocation().distanceTo(obj.getLocation()); - return getWorldLocation().distanceTo(obj.getWorldLocation()); + SWGObject tmp = this; + double selfX = 0.0; + double selfY = 0.0; + double selfZ = 0.0; + while (tmp != null) { + Location loc = tmp.getLocation(); + selfX += loc.getX(); + selfY += loc.getY(); + selfZ += loc.getZ(); + tmp = tmp.getParent(); + } + + tmp = obj; + double otherX = 0.0; + double otherY = 0.0; + double otherZ = 0.0; + while (tmp != null) { + Location loc = tmp.getLocation(); + otherX += loc.getX(); + otherY += loc.getY(); + otherZ += loc.getZ(); + tmp = tmp.getParent(); + } + + selfX -= otherX; + selfY -= otherY; + selfZ -= otherZ; + return Math.sqrt(selfX * selfX + selfY * selfY + selfZ * selfZ); } - + public double flatDistanceTo(@NotNull SWGObject obj) { - if (parent == obj.getParent()) - return getLocation().flatDistanceTo(obj.getLocation()); - return getWorldLocation().flatDistanceTo(obj.getWorldLocation()); + SWGObject tmp = this; + double selfX = 0.0; + double selfZ = 0.0; + while (tmp != null) { + Location loc = tmp.getLocation(); + selfX += loc.getX(); + selfZ += loc.getZ(); + tmp = tmp.getParent(); + } + + tmp = obj; + double otherX = 0.0; + double otherZ = 0.0; + while (tmp != null) { + Location loc = tmp.getLocation(); + otherX += loc.getX(); + otherZ += loc.getZ(); + tmp = tmp.getParent(); + } + + selfX -= otherX; + selfZ -= otherZ; + return Math.sqrt(selfX * selfX + selfZ * selfZ); } public double getX() { diff --git a/src/test/java/com/projectswg/holocore/resources/support/data/server_info/loader/NpcPatrolRouteLoaderTest.kt b/src/test/java/com/projectswg/holocore/resources/support/data/server_info/loader/NpcPatrolRouteLoaderTest.kt new file mode 100644 index 000000000..1ade37ab0 --- /dev/null +++ b/src/test/java/com/projectswg/holocore/resources/support/data/server_info/loader/NpcPatrolRouteLoaderTest.kt @@ -0,0 +1,108 @@ +/*********************************************************************************** + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore 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. * + * * + * Holocore 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 Holocore. If not, see . * + ***********************************************************************************/ +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