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 @@ -26,6 +26,8 @@
package com.projectswg.holocore.intents.gameplay.combat

import com.projectswg.common.data.location.Terrain
import com.projectswg.holocore.resources.gameplay.combat.CombatStatus
import com.projectswg.holocore.resources.support.global.commands.Command
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject
import com.projectswg.holocore.services.gameplay.combat.states.CombatState
Expand All @@ -40,6 +42,12 @@ data class CreatureRevivedIntent(val creature: CreatureObject) : Intent()
data class EnterCombatIntent(val source: TangibleObject, val target: TangibleObject) : Intent()
data class ExitCombatIntent(val source: TangibleObject) : Intent()
data class CloneActivatedIntent(val creature: CreatureObject, val diedOnTerrain: Terrain) : Intent()
data class CombatCommandFailedIntent(val source: CreatureObject, val status: CombatStatus) : Intent()

/*
* The command the client picked as its default attack, or null when it cleared the choice
*/
data class DefaultActionIntent(val creature: CreatureObject, val command: Command?) : Intent()

/*
* Combat event requests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/***********************************************************************************
* 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.resources.support.global.commands.callbacks.combat

import com.projectswg.holocore.intents.gameplay.combat.DefaultActionIntent
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
import com.projectswg.holocore.resources.support.global.commands.ICmdCallback
import com.projectswg.holocore.resources.support.global.player.Player
import com.projectswg.holocore.resources.support.objects.swg.SWGObject

class CmdDefaultAction : ICmdCallback {
override fun execute(player: Player, target: SWGObject?, args: String) {
val creature = player.creatureObject
val commandName = args.trim().lowercase()

if (commandName.isEmpty()) {
DefaultActionIntent(creature, null).broadcast()
return
}

val command = ServerData.commands.getCommand(commandName) ?: return
val grantedCommands = creature.commands.map { it.lowercase() }.toSet()

if (!grantedCommands.contains(commandName)) {
return
}

DefaultActionIntent(creature, command).broadcast()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public class CreatureObject extends TangibleObject {
private Race race = Race.HUMAN_MALE;
private long lastIncapTime = 0;
private TradeSession tradeSession = null;
private String defaultAttack = null;

private SWGSet<String> skills = new SWGSet<>(1, 3, StringType.ASCII);
private final AttributesMutable baseAttributes;
Expand Down Expand Up @@ -396,6 +397,18 @@ public void setTradeSession(TradeSession tradeSession) {
this.tradeSession = tradeSession;
}

/**
* @return name of the command to repeat while in combat, or {@code null} if no default attack is chosen
*/
@Nullable
public String getDefaultAttack() {
return defaultAttack;
}

public void setDefaultAttack(@Nullable String defaultAttack) {
this.defaultAttack = defaultAttack;
}

public void setPosture(Posture posture) {
creo3.setPosture(posture);
}
Expand Down Expand Up @@ -1069,6 +1082,7 @@ public void saveMongo(MongoData data) {
data.putString("race", race.name());
data.putArray("skills", skills);
data.putDocument("baseAttributes", baseAttributes);
data.putString("defaultAttack", defaultAttack);
Comment thread
Josh-Larson marked this conversation as resolved.
}

@Override
Expand All @@ -1082,6 +1096,7 @@ public void readMongo(MongoData data) {
race = Race.valueOf(data.getString("race", race.name()));
skills.addAll(data.getArray("skills", String.class));
data.getDocument("baseAttributes", baseAttributes);
defaultAttack = data.getString("defaultAttack");
}

private static class Container {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/***********************************************************************************
* 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.network.packets.swg.zone.ExecuteConsoleCommand
import com.projectswg.holocore.intents.gameplay.combat.CombatCommandFailedIntent
import com.projectswg.holocore.intents.gameplay.combat.DefaultActionIntent
import com.projectswg.holocore.intents.gameplay.combat.EnterCombatIntent
import com.projectswg.holocore.intents.gameplay.combat.ExitCombatIntent
import com.projectswg.holocore.intents.support.global.zone.PlayerEventIntent
import com.projectswg.holocore.resources.gameplay.combat.CombatStatus
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
import com.projectswg.holocore.resources.support.global.player.PlayerEvent
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject
import com.projectswg.holocore.utilities.HolocoreCoroutine
import com.projectswg.holocore.utilities.cancelAndWait
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import java.util.concurrent.ConcurrentHashMap

/**
* Repeats the default attack a player chose on their toolbar for as long as they are in combat.
* Clearing the choice stops the attacks, as does logging out or running out of action or mind.
*
* The choice itself lives on the [CreatureObject], because the client only sends it when a toolbar
* slot is ctrl-clicked or when it starts up.
*/
class AutoAttackService : Service() {

/** The job repeating the attack, for each creature currently auto-attacking. */
private val attackJobs: MutableMap<CreatureObject, Job> = ConcurrentHashMap()
private val coroutineScope = HolocoreCoroutine.childScope()

override fun terminate(): Boolean {
coroutineScope.cancelAndWait()
return super.terminate()
}

@IntentHandler
private fun handleDefaultActionIntent(dai: DefaultActionIntent) {
val creature = dai.creature
val command = dai.command

if (command == null) {
StandardLog.onPlayerTrace(this, creature, "cleared their default action")
creature.defaultAttack = null
cancelAttackJob(creature)
} else {
StandardLog.onPlayerTrace(this, creature, "set their default action to %s", command.name)
creature.defaultAttack = command.name
startAttackJob(creature)
}
}

@IntentHandler
private fun handleCombatCommandFailedIntent(ccfi: CombatCommandFailedIntent) {
if (ccfi.status == CombatStatus.TOO_TIRED) {
StandardLog.onPlayerTrace(this, ccfi.source, "stopped their default action, too tired")
cancelAttackJob(ccfi.source)
}
}

@IntentHandler
private fun handleEnterCombatIntent(eci: EnterCombatIntent) {
startAttackJob(eci.source as? CreatureObject)
}

@IntentHandler
private fun handleExitCombatIntent(eci: ExitCombatIntent) {
cancelAttackJob(eci.source as? CreatureObject)
}

@IntentHandler
private fun handlePlayerEventIntent(pei: PlayerEventIntent) {
when (pei.event) {
PlayerEvent.PE_LOGGED_OUT, PlayerEvent.PE_DESTROYED -> cancelAttackJob(pei.player.creatureObject)
else -> {}
}
}

private fun startAttackJob(creature: CreatureObject?) {
if (creature?.defaultAttack == null) {
return
}

synchronized(attackJobs) {
if (attackJobs[creature]?.isActive == true) {
return
}

attackJobs[creature] = coroutineScope.launch { requestAttacksWhileInCombat(creature) }
}
}

private fun cancelAttackJob(creature: CreatureObject?) {
if (creature == null) {
return
}

attackJobs.remove(creature)?.cancel()
}

private suspend fun requestAttacksWhileInCombat(creature: CreatureObject) {
while (creature.isInCombat) {
val defaultAttack = creature.defaultAttack ?: break
val weapon = creature.equippedWeapon ?: break
val owner = creature.owner ?: break

val executeConsoleCommand = ExecuteConsoleCommand()
executeConsoleCommand.addCommand(defaultAttack)
owner.sendPacket(executeConsoleCommand)

delay(attackDelay(creature, weapon))
}
}

private fun attackDelay(creature: CreatureObject, weapon: WeaponObject): Long {
return (weapon.getModdedWeaponAttackSpeedWithCap(creature) * 1000).toLong()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ import com.projectswg.holocore.services.gameplay.combat.loot.LootManager
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.ManagerStructure

@ManagerStructure(children = [BuffService::class, CloningService::class, DuelService::class, LootManager::class, CombatDeathblowService::class, CombatExperienceService::class, CombatNpcService::class, CombatRegenerationService::class, CombatStatusService::class, CombatKnockdownService::class, CombatStateService::class])
@ManagerStructure(children = [BuffService::class, CloningService::class, DuelService::class, LootManager::class, CombatDeathblowService::class, CombatExperienceService::class, CombatNpcService::class, CombatRegenerationService::class, CombatStatusService::class, CombatKnockdownService::class, CombatStateService::class, AutoAttackService::class])
class CombatManager : Manager()
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.projectswg.holocore.resources.support.global.commands.callbacks.chat.friend.*;
import com.projectswg.holocore.resources.support.global.commands.callbacks.combat.CmdAttack;
import com.projectswg.holocore.resources.support.global.commands.callbacks.combat.CmdCoupDeGrace;
import com.projectswg.holocore.resources.support.global.commands.callbacks.combat.CmdDefaultAction;
import com.projectswg.holocore.resources.support.global.commands.callbacks.combat.CmdDuel;
import com.projectswg.holocore.resources.support.global.commands.callbacks.combat.CmdEndDuel;
import com.projectswg.holocore.resources.support.global.commands.callbacks.conversation.*;
Expand Down Expand Up @@ -182,6 +183,7 @@ private void addCombatScripts() {
registerCppCallback("duel", CmdDuel::new);
registerCppCallback("endDuel", CmdEndDuel::new);
registerScriptCallback("attack", CmdAttack::new);
registerCppCallback("defaultAction", CmdDefaultAction::new);
}

private void addLootScripts() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.projectswg.common.data.combat.HitType
import com.projectswg.common.data.combat.TargetType
import com.projectswg.common.data.encodables.oob.StringId
import com.projectswg.common.network.packets.swg.zone.object_controller.*
import com.projectswg.holocore.intents.gameplay.combat.CombatCommandFailedIntent
import com.projectswg.holocore.intents.gameplay.combat.ExitCombatIntent
import com.projectswg.holocore.intents.support.global.command.ExecuteCommandIntent
import com.projectswg.holocore.intents.support.global.command.QueueCommandIntent
Expand Down Expand Up @@ -225,6 +226,7 @@ class CommandQueueService @JvmOverloads constructor(private val delayBetweenChec

handleStatus(source, combatCommand, combatStatus)
if (combatStatus != CombatStatus.SUCCESS) {
CombatCommandFailedIntent(source, combatStatus).broadcast()
sendCommandFailed(command)
return
}
Expand Down