Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.projectswg.holocore.intents.support.global.command.ExecuteCommandInte
import com.projectswg.holocore.intents.support.global.command.QueueCommandIntent
import com.projectswg.holocore.intents.support.global.network.InboundPacketIntent
import com.projectswg.holocore.intents.support.global.zone.PlayerEventIntent
import com.projectswg.holocore.intents.support.objects.MoveObjectIntent
import com.projectswg.holocore.resources.gameplay.combat.CombatStatus
import com.projectswg.holocore.resources.support.color.SWGColor.Whites.white
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
Expand All @@ -58,6 +59,7 @@ import com.projectswg.holocore.utilities.HolocoreCoroutine
import com.projectswg.holocore.utilities.cancelAndWait
import com.projectswg.holocore.utilities.launchAfter
import com.projectswg.holocore.utilities.launchWithFixedRate
import kotlinx.coroutines.Job
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import me.joshlarson.jlcommon.log.Log
Expand Down Expand Up @@ -121,6 +123,15 @@ class CommandQueueService @JvmOverloads constructor(private val delayBetweenChec
getQueue(qci.source).queueCommand(EnqueuedCommand(qci.source, qci.command, qci.target, qci.arguments, qci.counter))
}

@IntentHandler
private fun handleMoveObjectIntent(moi: MoveObjectIntent) {
// A speed of zero means the creature is turning in place, which should not interrupt a warmup
if (moi.speed <= 0) return
val creature = moi.obj as? CreatureObject ?: return

combatQueueMap[creature]?.cancelWarmup()
}

@IntentHandler
private fun handleExitCombatIntent(eci: ExitCombatIntent) {
if (eci.source is CreatureObject) {
Expand All @@ -139,6 +150,7 @@ class CommandQueueService @JvmOverloads constructor(private val delayBetweenChec
private inner class CreatureCombatQueue {
private val commandQueue: Queue<EnqueuedCommand> = PriorityQueue()
private val activeCooldownGroups: MutableSet<String> = ConcurrentHashMap.newKeySet()
private var warmup: Warmup? = null

@Synchronized
fun executeNextCommand() {
Expand Down Expand Up @@ -197,14 +209,34 @@ class CommandQueueService @JvmOverloads constructor(private val delayBetweenChec

command.source.sendSelf(warmupTimer)

coroutineScope.launchAfter((warmupTime * 1000).toLong()) {
executeCommandNow(command)
val job = coroutineScope.launchAfter((warmupTime * 1000).toLong()) {
finishWarmup(command)
}
warmup = Warmup(command, job)
} else {
executeCommandNow(command)
}
}

@Synchronized
private fun finishWarmup(command: EnqueuedCommand) {
// The warmup can be cancelled while this coroutine waits for the lock
if (warmup?.command !== command) return
warmup = null

executeCommandNow(command)
}

@Synchronized
fun cancelWarmup() {
val cancelled = warmup ?: return
warmup = null
cancelled.job.cancel()

sendQueueRemove(cancelled.command, CheckCommandResult(CommandQueueDequeue.ErrorCode.CANCELLED, 0))
sendCommandFailed(cancelled.command)
}

private fun executeCommandNow(command: EnqueuedCommand) {
val rootCommand: Command = command.command
val source: CreatureObject = command.source
Expand Down Expand Up @@ -394,6 +426,8 @@ class CommandQueueService @JvmOverloads constructor(private val delayBetweenChec
}
}

private class Warmup(val command: EnqueuedCommand, val job: Job)

private class EnqueuedCommand(val source: CreatureObject, val command: Command, val target: SWGObject?, val arguments: String, val counter: Int) : Comparable<EnqueuedCommand> {
override fun compareTo(other: EnqueuedCommand): Int {
return command.defaultPriority.compareTo(other.command.defaultPriority)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/***********************************************************************************
* Copyright (c) 2026 /// 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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
package com.projectswg.holocore.services.gameplay.combat

import com.projectswg.common.data.CRC
import com.projectswg.common.network.packets.swg.zone.object_controller.CommandQueueEnqueue
import com.projectswg.common.network.packets.swg.zone.object_controller.CommandTimer
import com.projectswg.holocore.intents.support.global.network.InboundPacketIntent
import com.projectswg.holocore.intents.support.objects.MoveObjectIntent
import com.projectswg.holocore.intents.support.objects.ObjectCreatedIntent
import com.projectswg.holocore.resources.support.objects.ObjectCreator
import com.projectswg.holocore.resources.support.objects.swg.weapon.DefaultWeaponFactory
import com.projectswg.holocore.services.support.global.commands.CommandExecutionService
import com.projectswg.holocore.services.support.global.commands.CommandQueueService
import com.projectswg.holocore.test.resources.GenericCreatureObject
import com.projectswg.holocore.test.runners.TestRunnerSimulatedWorld
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit

/**
* A command with a warmup time is used, so there is a window where the warmup can be interrupted.
*/
class WarmupTest : TestRunnerSimulatedWorld() {

@BeforeEach
fun setup() {
registerService(CommandQueueService(5))
registerService(CommandExecutionService())
}

@Test
fun `moving interrupts the warmup`() {
val creatureObject = createCreatureObject()
startWarmup(creatureObject)

MoveObjectIntent(creatureObject, creatureObject.location, RUN_SPEED).broadcast()
waitForIntents()

assertNotNull(waitForCommandTimerWithFlag(creatureObject, CommandTimer.CommandTimerFlag.FAILED), "Warmup was not interrupted")
}

@Test
fun `turning in place does not interrupt the warmup`() {
val creatureObject = createCreatureObject()
startWarmup(creatureObject)

MoveObjectIntent(creatureObject, creatureObject.location, 0.0).broadcast()
waitForIntents()

assertNull(waitForCommandTimerWithFlag(creatureObject, CommandTimer.CommandTimerFlag.FAILED), "Warmup was interrupted")
}

private fun startWarmup(creatureObject: GenericCreatureObject) {
val player = creatureObject.owner ?: throw RuntimeException("Unable to access player")
val crc = CRC.getCrc("coupdegrace")
InboundPacketIntent(player, CommandQueueEnqueue(creatureObject.objectId, 0, crc, 0, "")).broadcast()

assertNotNull(waitForCommandTimerWithFlag(creatureObject, CommandTimer.CommandTimerFlag.WARMUP), "Warmup never started")
}

private fun waitForCommandTimerWithFlag(creatureObject: GenericCreatureObject, flag: CommandTimer.CommandTimerFlag): CommandTimer? {
val player = creatureObject.owner ?: throw RuntimeException("Unable to access player")
return player.waitForNextPacket(CommandTimer::class.java, 500, TimeUnit.MILLISECONDS) { it.flags.contains(flag) }
}

private fun createCreatureObject(): GenericCreatureObject {
val creatureObject = GenericCreatureObject(ObjectCreator.getNextObjectId())
ObjectCreatedIntent(creatureObject).broadcast()
val defaultWeapon = DefaultWeaponFactory.createDefaultWeapon()
defaultWeapon.moveToContainer(creatureObject)
creatureObject.equippedWeapon = defaultWeapon
return creatureObject
}

private companion object {
private const val RUN_SPEED = 5.376
}
}