Skip to content

Commit e7496db

Browse files
committed
Fixed or improved a variety of CPU and RAM issues
1 parent 1175485 commit e7496db

7 files changed

Lines changed: 196 additions & 27 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ classes/
88
.classpath
99
.project
1010
.kotlin
11+
/.idea/AndroidProjectSystem.xml
1112
# IntelliJ-User Specific
1213
.idea/**/workspace.xml
1314
.idea/**/tasks.xml

src/main/java/com/projectswg/holocore/resources/support/npc/ai/NavigationPoint.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,13 @@ class NavigationPoint(val parent: SWGObject?, val location: Location, val speed:
120120
}
121121

122122
fun from(sourceParent: SWGObject?, source: Location, destinationParent: SWGObject?, destination: Location, speed: Double): List<NavigationPoint> {
123+
assert(sourceParent == null || sourceParent is CellObject) { "invalid source parent" }
124+
assert(destinationParent == null || destinationParent is CellObject) { "invalid destination parent" }
125+
assert(speed > 0) { "speed must be greater than zero, was $speed" }
126+
127+
if (sourceParent == destinationParent) return from(sourceParent, source, destination, speed)
128+
123129
var source = source
124-
assert(sourceParent == null || sourceParent is CellObject)
125-
assert(destinationParent == null || destinationParent is CellObject)
126130
val route = getBuildingRoute(sourceParent as CellObject?, destinationParent as CellObject?, source, destination) ?: return ArrayList()
127131
val points = createIntraBuildingRoute(route, sourceParent, source, speed)
128132
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:
145149
val totalDistance = source.distanceTo(destination)
146150
val path: MutableList<NavigationPoint> = ArrayList()
147151

152+
assert(speed > 0) { "speed must be greater than zero, was $speed" }
153+
assert(totalDistance < 5_000) { "distance between waypoints is too large ($totalDistance)" }
154+
148155
var currentDistance = speed
149156
while (currentDistance < totalDistance) {
150157
path.add(interpolate(parent, source, destination, speed, currentDistance / totalDistance))
151158
currentDistance += speed
159+
assert(path.size < 10_000) { "path length growing too large" }
152160
}
153161
path.add(interpolate(parent, source, destination, speed, 1.0))
154162
return path
@@ -229,7 +237,7 @@ class NavigationPoint(val parent: SWGObject?, val location: Location, val speed:
229237

230238
private fun buildWorldPortalLocation(portal: Portal): Location {
231239
val building = portal.cell1!!.parent
232-
assert(building is BuildingObject)
240+
assert(building is BuildingObject) { "cell parent wasn't a building" }
233241
return Location.builder(buildPortalLocation(portal)).translateLocation(building!!.location).build()
234242
}
235243

src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcPatrolMode.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,19 @@ class NpcPatrolMode(obj: AIObject, waypoints: List<ResolvedPatrolWaypoint>) : Np
5151
waypointBuilder.add(waypointBuilder[0])
5252
}
5353

54-
this.waypoints = ArrayList<NavigationPoint>(128)
55-
for (i in 1 until waypointBuilder.size) {
56-
val source = waypointBuilder[i - 1]
57-
val destination = waypointBuilder[i]
58-
this.waypoints.addAll(NavigationPoint.from(source.parent, source.location, destination.parent, destination.location, walkSpeed))
59-
if (destination.delay > 0)
60-
this.waypoints.addAll(NavigationPoint.nop(this.waypoints[this.waypoints.size - 1], destination.delay.toInt() - 1))
54+
if (waypointBuilder.isEmpty()) {
55+
this.waypoints = ArrayList<NavigationPoint>(128)
56+
} else {
57+
this.waypoints = ArrayList<NavigationPoint>(128)
58+
for (i in 1 until waypointBuilder.size) {
59+
val source = waypointBuilder[i - 1]
60+
val destination = waypointBuilder[i]
61+
this.waypoints.addAll(NavigationPoint.from(source.parent, source.location, destination.parent, destination.location, walkSpeed))
62+
if (destination.delay > 0)
63+
this.waypoints.addAll(NavigationPoint.nop(this.waypoints[this.waypoints.size - 1], destination.delay.toInt() - 1))
64+
}
65+
this.waypoints.addAll(NavigationPoint.from(waypointBuilder[waypointBuilder.size - 1].parent, waypointBuilder[waypointBuilder.size - 1].location, waypointBuilder[0].parent, waypointBuilder[0].location, walkSpeed))
6166
}
62-
this.waypoints.addAll(NavigationPoint.from(waypointBuilder[waypointBuilder.size - 1].parent, waypointBuilder[waypointBuilder.size - 1].location, waypointBuilder[0].parent, waypointBuilder[0].location, walkSpeed))
6367
}
6468

6569
override suspend fun onModeStart() {

src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@ import com.projectswg.holocore.resources.support.objects.permissions.AdminPermis
3939
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty
4040
import com.projectswg.holocore.resources.support.objects.swg.custom.AIBehavior
4141
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
42-
import java.util.concurrent.ThreadLocalRandom
4342
import kotlin.math.cos
43+
import kotlin.math.max
44+
import kotlin.math.min
4445
import kotlin.math.sin
46+
import kotlin.random.Random
4547

4648
class DynamicMovementObject(var location: Location, val name: String, val baseSpeed: Double = 0.0) {
47-
48-
var heading = ThreadLocalRandom.current().nextDouble() * 2 * Math.PI
49+
50+
private val random = Random(System.currentTimeMillis())
51+
var heading = random.nextDouble() * 2 * Math.PI
4952
private val groupMarker = ObjectCreator.createObjectFromTemplate("object/path_waypoint/shared_path_waypoint_droid.iff")
5053
private val npcs = ArrayList<AIObject>()
5154
private var lastUpdate = System.nanoTime()
@@ -70,7 +73,6 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp
7073
val bossSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.BOSS).build(), groupMarker)
7174
val eliteSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.ELITE).build(), groupMarker)
7275
val normalSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.NORMAL).build(), groupMarker)
73-
val random = ThreadLocalRandom.current()
7476
if (random.nextDouble() < 0.25)
7577
npcs.add(NPCCreator.createSingleNpc(bossSpawner))
7678
npcs.add(NPCCreator.createSingleNpc(eliteSpawner))
@@ -102,7 +104,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp
102104
.setZ(location.z + radius * sin(angle))
103105
newLocationBuilder.setY(ServerData.terrains.getHeight(newLocationBuilder))
104106
val newLocation = newLocationBuilder.build()
105-
val speed = it.worldLocation.distanceTo(newLocation) / elapsedTime
107+
val speed = min(30.0, max(1.0, it.worldLocation.distanceTo(newLocation) / elapsedTime))
106108
it.moveTo(null, newLocationBuilder.build(), speed)
107109
}
108110
}
@@ -114,7 +116,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp
114116
return
115117
}
116118

117-
val newHeading = heading + Math.PI * (1 + ThreadLocalRandom.current().nextDouble() - 0.5)
119+
val newHeading = heading + Math.PI * (1 + random.nextDouble() - 0.5)
118120
val secondProposed = calculateNextPosition(newHeading, distance)
119121
if (isValidNextPosition(secondProposed)) {
120122
location = secondProposed
@@ -123,7 +125,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp
123125
}
124126

125127
// Brute Force Escape
126-
val randomRotationFromNorth = ThreadLocalRandom.current().nextDouble() * Math.TAU
128+
val randomRotationFromNorth = random.nextDouble() * Math.TAU
127129
for (clockwiseRotation in 0..35) {
128130
val bruteForceHeading = (clockwiseRotation * 10) * Math.PI / 180.0 + randomRotationFromNorth
129131
val proposed = calculateNextPosition(bruteForceHeading, distance)
@@ -135,6 +137,7 @@ class DynamicMovementObject(var location: Location, val name: String, val baseSp
135137
}
136138

137139
// TODO: destroy this object, we got stuck
140+
location = firstProposed
138141
assert(false)
139142
}
140143

src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -796,17 +796,62 @@ public Location getLocation() {
796796
public Location getWorldLocation() {
797797
return location.getWorldLocation(this);
798798
}
799-
799+
800800
public double distanceTo(@NotNull SWGObject obj) {
801-
if (parent == obj.getParent())
802-
return getLocation().distanceTo(obj.getLocation());
803-
return getWorldLocation().distanceTo(obj.getWorldLocation());
801+
SWGObject tmp = this;
802+
double selfX = 0.0;
803+
double selfY = 0.0;
804+
double selfZ = 0.0;
805+
while (tmp != null) {
806+
Location loc = tmp.getLocation();
807+
selfX += loc.getX();
808+
selfY += loc.getY();
809+
selfZ += loc.getZ();
810+
tmp = tmp.getParent();
811+
}
812+
813+
tmp = obj;
814+
double otherX = 0.0;
815+
double otherY = 0.0;
816+
double otherZ = 0.0;
817+
while (tmp != null) {
818+
Location loc = tmp.getLocation();
819+
otherX += loc.getX();
820+
otherY += loc.getY();
821+
otherZ += loc.getZ();
822+
tmp = tmp.getParent();
823+
}
824+
825+
selfX -= otherX;
826+
selfY -= otherY;
827+
selfZ -= otherZ;
828+
return Math.sqrt(selfX * selfX + selfY * selfY + selfZ * selfZ);
804829
}
805-
830+
806831
public double flatDistanceTo(@NotNull SWGObject obj) {
807-
if (parent == obj.getParent())
808-
return getLocation().flatDistanceTo(obj.getLocation());
809-
return getWorldLocation().flatDistanceTo(obj.getWorldLocation());
832+
SWGObject tmp = this;
833+
double selfX = 0.0;
834+
double selfZ = 0.0;
835+
while (tmp != null) {
836+
Location loc = tmp.getLocation();
837+
selfX += loc.getX();
838+
selfZ += loc.getZ();
839+
tmp = tmp.getParent();
840+
}
841+
842+
tmp = obj;
843+
double otherX = 0.0;
844+
double otherZ = 0.0;
845+
while (tmp != null) {
846+
Location loc = tmp.getLocation();
847+
otherX += loc.getX();
848+
otherZ += loc.getZ();
849+
tmp = tmp.getParent();
850+
}
851+
852+
selfX -= otherX;
853+
selfZ -= otherZ;
854+
return Math.sqrt(selfX * selfX + selfZ * selfZ);
810855
}
811856

812857
public double getX() {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/***********************************************************************************
2+
* Copyright (c) 2025 /// Project SWG /// www.projectswg.com *
3+
* *
4+
* ProjectSWG is an emulation project for Star Wars Galaxies founded on *
5+
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
6+
* Our goal is to create one or more emulators which will provide servers for *
7+
* players to continue playing a game similar to the one they used to play. *
8+
* *
9+
* This file is part of Holocore. *
10+
* *
11+
* --------------------------------------------------------------------------------*
12+
* *
13+
* Holocore is free software: you can redistribute it and/or modify *
14+
* it under the terms of the GNU Affero General Public License as *
15+
* published by the Free Software Foundation, either version 3 of the *
16+
* License, or (at your option) any later version. *
17+
* *
18+
* Holocore is distributed in the hope that it will be useful, *
19+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
20+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
21+
* GNU Affero General Public License for more details. *
22+
* *
23+
* You should have received a copy of the GNU Affero General Public License *
24+
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
25+
***********************************************************************************/
26+
package com.projectswg.holocore.resources.support.data.server_info.loader
27+
28+
import com.projectswg.common.data.location.Location
29+
import com.projectswg.common.data.swgiff.parsers.SWGParser
30+
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcPatrolRouteLoader
31+
import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint
32+
import com.projectswg.holocore.resources.support.npc.spawn.Spawner
33+
import com.projectswg.holocore.services.support.objects.ObjectStorageService
34+
import com.projectswg.holocore.test.runners.TestRunnerNoIntents
35+
import org.junit.jupiter.api.AfterAll
36+
import org.junit.jupiter.api.Assertions
37+
import org.junit.jupiter.api.BeforeAll
38+
import org.junit.jupiter.api.Test
39+
import java.util.concurrent.atomic.AtomicBoolean
40+
41+
class NpcPatrolRouteLoaderTest : TestRunnerNoIntents() {
42+
43+
@Test
44+
fun `test patrol route waypoints`() {
45+
fun checkRouteLeg(sourceWaypoint: Spawner.ResolvedPatrolWaypoint, destinationWaypoint: Spawner.ResolvedPatrolWaypoint) {
46+
val route = NavigationPoint.from(sourceWaypoint.parent, sourceWaypoint.location, destinationWaypoint.parent, destinationWaypoint.location, 1.0)
47+
assert(route.size < 500) { "distance between waypoints is too large (${route.size})" }
48+
assert(sourceWaypoint.location.terrain == destinationWaypoint.location.terrain) { "terrain mismatch along route" }
49+
}
50+
51+
val hasError = AtomicBoolean(false)
52+
ServerData.npcPatrolRoutes.forEach { route ->
53+
try {
54+
val resolvedRoute = route.map { Spawner.ResolvedPatrolWaypoint(it) }
55+
assert(route.isNotEmpty()) { "route is empty" }
56+
for (i in 1 until route.size) {
57+
checkRouteLeg(resolvedRoute[i - 1], resolvedRoute[i])
58+
}
59+
if (route[0].patrolType == NpcPatrolRouteLoader.PatrolType.LOOP) checkRouteLeg(resolvedRoute[route.size - 1], resolvedRoute[0])
60+
} catch (e: AssertionError) {
61+
System.err.println("Patrol group '${route[0].groupId}' error: ${e.message}")
62+
hasError.set(true)
63+
}
64+
}
65+
Assertions.assertFalse(hasError.get())
66+
}
67+
68+
@Test
69+
fun `test NPC to patrol route start`() {
70+
val hasError = AtomicBoolean(false)
71+
ServerData.npcStaticSpawns.spawns.parallelStream().forEach { spawn ->
72+
if (spawn.patrolId.isEmpty() || spawn.patrolId == "0") return@forEach
73+
val spawnerLocation = Location.builder().setTerrain(spawn.terrain).setX(spawn.x).setY(spawn.y).setZ(spawn.z).build()
74+
val route = ServerData.npcPatrolRoutes[spawn.patrolId]
75+
val routeLocation = Location.builder().setTerrain(route[0].terrain).setX(route[0].x).setY(route[0].y).setZ(route[0].z).build()
76+
val distanceToRoute = spawnerLocation.distanceTo(routeLocation)
77+
try {
78+
assert(spawn.buildingId == route[0].buildingId) { "NPC not in same building as route" }
79+
assert(spawn.cellId == route[0].cellId) { "NPC not in same cell as route" }
80+
assert(distanceToRoute < 500) { "Spawner distance to route too large ($distanceToRoute)" }
81+
assert(spawnerLocation.terrain == routeLocation.terrain) { "terrain mismatch along route" }
82+
} catch (e: AssertionError) {
83+
System.err.println("Patrol spawner '${spawn.npcId}' with route '${spawn.patrolId}' error: ${e.message}")
84+
hasError.set(true)
85+
}
86+
}
87+
Assertions.assertFalse(hasError.get())
88+
}
89+
90+
companion object {
91+
92+
private var objectStorageService = ObjectStorageService()
93+
94+
@BeforeAll
95+
@JvmStatic
96+
fun setup() {
97+
SWGParser.setBasePath("serverdata")
98+
objectStorageService.initialize()
99+
}
100+
101+
@AfterAll
102+
@JvmStatic
103+
fun tearDown() {
104+
objectStorageService.terminate()
105+
}
106+
}
107+
108+
}

0 commit comments

Comments
 (0)