From c7fe51c96b1390475d001c419cab5aeb92e2500f Mon Sep 17 00:00:00 2001 From: Obique Date: Sun, 1 Sep 2019 16:51:28 -0400 Subject: [PATCH] Fixed dueling and added NPC-NPC combat --- pswgcommon | 2 +- .../gameplay/gcw/faction/FactionIntent.java | 10 +- .../gameplay/combat/EnemyProcessor.kt | 101 ++++++++++++++ .../data/server_info/loader/ServerData.kt | 2 + .../loader/combat/FactionLoader.kt | 102 ++++++++++++++ .../server_info/loader/npc/NpcLoader.java | 20 +-- .../commands/callbacks/combat/CmdPVP.java | 7 +- .../support/npc/ai/NpcCombatMode.java | 4 +- .../support/npc/spawn/NPCCreator.java | 26 ++-- .../resources/support/npc/spawn/Spawner.java | 4 +- .../objects/awareness/TerrainMapChunk.java | 117 ---------------- .../objects/awareness/TerrainMapChunk.kt | 132 ++++++++++++++++++ .../support/objects/swg/SWGObject.java | 16 +++ .../objects/swg/creature/CreatureObject.java | 59 +------- .../swg/creature/CreatureObjectAwareness.java | 49 +++++++ .../support/objects/swg/custom/AIObject.java | 87 +++++++----- .../objects/swg/tangible/TangibleObject.java | 103 ++++++-------- .../combat/CombatDeathblowService.java | 2 +- .../gameplay/combat/CombatStatusService.java | 77 ---------- .../gameplay/combat/CombatStatusService.kt | 116 +++++++++++++++ .../combat/command/CombatCommandCommon.java | 9 +- .../combat/command/CombatCommandHeal.java | 3 +- .../gameplay/combat/duel/DuelService.java | 13 +- .../gameplay/gcw/faction/CivilWarService.java | 2 +- .../gcw/faction/FactionFlagService.java | 5 +- .../world/travel/PlayerMountService.java | 2 +- .../support/data/dev/DeveloperService.java | 2 +- .../global/commands/CommandQueueService.java | 2 +- .../gameplay/combat/TestEnemyProcessor.kt | 124 ++++++++++++++++ .../objects/swg/TestSWGPersistence.java | 2 +- .../test/resources/GenericPlayer.java | 3 +- 31 files changed, 786 insertions(+), 417 deletions(-) create mode 100644 src/main/java/com/projectswg/holocore/resources/gameplay/combat/EnemyProcessor.kt create mode 100644 src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/combat/FactionLoader.kt delete mode 100644 src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.java create mode 100644 src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.kt delete mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.java create mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.kt create mode 100644 src/test/java/com/projectswg/holocore/resources/gameplay/combat/TestEnemyProcessor.kt diff --git a/pswgcommon b/pswgcommon index b7afb606f..e34be4617 160000 --- a/pswgcommon +++ b/pswgcommon @@ -1 +1 @@ -Subproject commit b7afb606fcae8c0136ac16c8475036e25c5a7d72 +Subproject commit e34be4617933a48d1658e15f467ccb9291824865 diff --git a/src/main/java/com/projectswg/holocore/intents/gameplay/gcw/faction/FactionIntent.java b/src/main/java/com/projectswg/holocore/intents/gameplay/gcw/faction/FactionIntent.java index 150cd9a63..75dc725a2 100644 --- a/src/main/java/com/projectswg/holocore/intents/gameplay/gcw/faction/FactionIntent.java +++ b/src/main/java/com/projectswg/holocore/intents/gameplay/gcw/faction/FactionIntent.java @@ -26,15 +26,15 @@ ***********************************************************************************/ package com.projectswg.holocore.intents.gameplay.gcw.faction; -import com.projectswg.common.data.encodables.tangible.PvpFaction; import com.projectswg.common.data.encodables.tangible.PvpStatus; +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader.Faction; import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject; import me.joshlarson.jlcommon.control.Intent; public class FactionIntent extends Intent { private TangibleObject target; - private PvpFaction newFaction; + private Faction newFaction; private FactionIntentType updateType; private PvpStatus newStatus; @@ -47,7 +47,7 @@ public class FactionIntent extends Intent { this.updateType = updateType; } - public FactionIntent(TangibleObject target, PvpFaction newFaction) { + public FactionIntent(TangibleObject target, Faction newFaction) { this(target, FactionIntentType.FACTIONUPDATE); this.newFaction = newFaction; } @@ -61,7 +61,7 @@ public class FactionIntent extends Intent { return target; } - public PvpFaction getNewFaction() { + public Faction getNewFaction() { return newFaction; } @@ -81,7 +81,7 @@ public class FactionIntent extends Intent { new FactionIntent(target, FactionIntentType.STATUSUPDATE).broadcast(); } - public static void broadcastUpdateFaction(TangibleObject target, PvpFaction newFaction) { + public static void broadcastUpdateFaction(TangibleObject target, Faction newFaction) { new FactionIntent(target, newFaction).broadcast(); } diff --git a/src/main/java/com/projectswg/holocore/resources/gameplay/combat/EnemyProcessor.kt b/src/main/java/com/projectswg/holocore/resources/gameplay/combat/EnemyProcessor.kt new file mode 100644 index 000000000..de1efa326 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/gameplay/combat/EnemyProcessor.kt @@ -0,0 +1,101 @@ +/*********************************************************************************** + * 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.resources.gameplay.combat + +import com.projectswg.common.data.encodables.tangible.Posture +import com.projectswg.common.data.encodables.tangible.PvpStatus +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.tangible.OptionFlag +import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject + +object EnemyProcessor { + + fun isAttackable(source: TangibleObject, target: TangibleObject): Boolean { + if (target.hasOptionFlags(OptionFlag.INVULNERABLE)) + return false + if (source !is CreatureObject || target !is CreatureObject) + return isValidTangible(source) && isValidTangible(target) && isFactionAttackable(source, target) + + if (!isValidPosture(source) || !isValidPosture(target)) + return false // If neither of us have a valid posture, nobody can attack + if (isAttackAllowedAnyways(source, target)) + return true + + // NPC-based checks + if (source is AIObject) { + if (target is AIObject) + return isFactionAttackable(source, target) + return ((source.hasOptionFlags(OptionFlag.AGGRESSIVE) || source.defenders.contains(target.objectId)) && isFactionAttackable(target, source)) || isFactionAttackable(source, target) // EvP only when aggressive or under attack + } else if (target is AIObject) { + return isFactionAttackable(source, target) // PvE allowed + } + return isFactionAttackable(source, target) // PvP + } + + private fun isFactionAttackable(source: TangibleObject, target: TangibleObject): Boolean { + val ourFaction = source.faction + val otherFaction = target.faction + + if (ourFaction == null || ourFaction.name == "neutral" || otherFaction == null || otherFaction.name == "neutral") + return source is CreatureObject && source.isPlayer && target is CreatureObject && !target.isPlayer // either neutral = PvE only + + // PvE / EvE - where one is a TangibleObject + if (source !is CreatureObject || target !is CreatureObject) + return ourFaction.isEnemy(otherFaction) || (source is CreatureObject && source.isPlayer && !ourFaction.isAlly(otherFaction)) + // PvP + if (source.isPlayer && target.isPlayer) + return ourFaction.isEnemy(otherFaction) && source.pvpStatus == PvpStatus.SPECIALFORCES && target.pvpStatus == PvpStatus.SPECIALFORCES + // PvE + if (source.isPlayer) + return !ourFaction.isAlly(otherFaction) + // EvE + return ourFaction.isEnemy(otherFaction) + } + + private fun isAttackAllowedAnyways(source: CreatureObject, target: CreatureObject): Boolean { + // TODO bounty hunting + // TODO pets, vehicles etc having same flagging as their owner + // TODO guild wars + if (source.isDuelingPlayer(target)) + return true // If we're dueling, can always attack + return false + } + + private fun isValidTangible(obj: TangibleObject): Boolean { + return obj !is CreatureObject || isValidPosture(obj) + } + + private fun isValidPosture(obj: CreatureObject): Boolean { + return when (obj.posture) { + Posture.DEAD, Posture.INCAPACITATED -> false + else -> true + } + } + +} diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt index 2c60bb982..60683c7ea 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt @@ -27,6 +27,7 @@ package com.projectswg.holocore.resources.support.data.server_info.loader +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader import com.projectswg.holocore.resources.support.data.server_info.loader.npc.* import me.joshlarson.jlcommon.log.Log import java.io.IOException @@ -42,6 +43,7 @@ object ServerData { */ val buffs by SoftDataLoaderDelegate(::BuffLoader) val specialLines by SoftDataLoaderDelegate(::SpecialLineLoader) + val factions by SoftDataLoaderDelegate(::FactionLoader) /* * Skill / Expertise / Collection diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/combat/FactionLoader.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/combat/FactionLoader.kt new file mode 100644 index 000000000..c3bc5b97f --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/combat/FactionLoader.kt @@ -0,0 +1,102 @@ +/*********************************************************************************** + * 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.resources.support.data.server_info.loader.combat + +import com.projectswg.common.data.encodables.tangible.PvpFaction +import com.projectswg.holocore.resources.support.data.server_info.SdbLoader +import com.projectswg.holocore.resources.support.data.server_info.SdbLoader.SdbResultSet +import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader +import java.io.File +import java.io.IOException +import java.util.* +import java.util.stream.Collectors + +class FactionLoader : DataLoader() { + + private var factions: Map = HashMap() + + fun getFaction(faction: String): Faction? { + return factions[faction] + } + + @Throws(IOException::class) + override fun load() { + SdbLoader.load(File("serverdata/faction/faction_datatable.sdb")).use { set -> + factions = set.stream { Faction(it) }.collect(Collectors.toMap({ it.name }, { it })) + } + } + + class Faction(set: SdbResultSet) { + + val name = set.getText("factionName").toLowerCase(Locale.US) + val isPlayerAllowed = set.getBoolean("playerAllowed") + val isAggressive = set.getBoolean("isAggro") + val isPvP = set.getBoolean("isPvP") + val pvpFaction = processPvpFaction(set.getText("pvpFaction")) + private val enemies = processList(set.getText("enemies")) + private val allies = processList(set.getText("allies")) + val combatFactor = set.getInt("combatFactor").toInt() + + fun isEnemy(faction: String) = enemies.contains(faction.toLowerCase(Locale.US)) + fun isEnemy(faction: Faction) = enemies.contains(faction.name) + + fun isAlly(faction: String) = name == faction.toLowerCase() || allies.contains(faction.toLowerCase(Locale.US)) + fun isAlly(faction: Faction) = name == faction.name || allies.contains(faction.name) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return name == (other as Faction).name + } + + override fun hashCode(): Int { + return name.hashCode() + } + + override fun toString(): String { + return "Faction(name=$name isPlayerAllowed=$isPlayerAllowed isAggressive=$isAggressive isPvP=$isPvP enemies=$enemies allies=$allies)" + } + + private fun processPvpFaction(faction: String): PvpFaction { + return when (faction.toLowerCase(Locale.US)) { + "imperial" -> PvpFaction.IMPERIAL + "rebel" -> PvpFaction.REBEL + else -> PvpFaction.NEUTRAL + } + } + + private fun processList(listStr: String): List { + if (listStr == "-" || listStr.isBlank()) + return emptyList() + return listStr.toLowerCase(Locale.US).split(';') + } + + } + +} diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcLoader.java b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcLoader.java index 0e89dc6d6..89a945184 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcLoader.java +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcLoader.java @@ -26,11 +26,12 @@ ***********************************************************************************/ package com.projectswg.holocore.resources.support.data.server_info.loader.npc; -import com.projectswg.common.data.encodables.tangible.PvpFaction; import com.projectswg.common.data.swgfile.ClientFactory; import com.projectswg.holocore.resources.support.data.server_info.SdbLoader; import com.projectswg.holocore.resources.support.data.server_info.SdbLoader.SdbResultSet; import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader; +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData; +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader.Faction; import java.io.File; import java.io.IOException; @@ -122,7 +123,7 @@ public final class NpcLoader extends DataLoader { private final String name; private final String stfName; private final String niche; - private final PvpFaction faction; + private final Faction faction; private final boolean specForce; private final double attackSpeed; private final double movementSpeed; @@ -156,6 +157,7 @@ public final class NpcLoader extends DataLoader { this.hue = (int) set.getInt("hue"); this.stfName = set.getText("stf_name"); this.niche = set.getText("niche").intern(); + this.faction = ServerData.INSTANCE.getFactions().getFaction(set.getText("faction")); this.iffs = List.of(set.getText("iff_template").split(";")).stream().map(s -> "object/mobile/"+s).map(ClientFactory::formatToSharedFile).collect(Collectors.toUnmodifiableList()); this.specForce = set.getBoolean("spec_force"); this.attackSpeed = set.getReal("attack_speed"); @@ -177,18 +179,6 @@ public final class NpcLoader extends DataLoader { if (scaleMax < scaleMin) throw new IllegalArgumentException("scaleMax must be greater than scaleMin"); - switch (set.getText("faction")) { - case "rebel": - this.faction = PvpFaction.REBEL; - break; - case "imperial": - this.faction = PvpFaction.IMPERIAL; - break; - default: - this.faction = PvpFaction.NEUTRAL; - break; - } - switch (niche) { case "humanoid": this.humanoidInfo = new HumanoidNpcInfo(set); @@ -231,7 +221,7 @@ public final class NpcLoader extends DataLoader { return Collections.unmodifiableList(iffs); } - public PvpFaction getFaction() { + public Faction getFaction() { return faction; } diff --git a/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/combat/CmdPVP.java b/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/combat/CmdPVP.java index 7c6af31b1..9a33ae1e5 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/combat/CmdPVP.java +++ b/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/combat/CmdPVP.java @@ -3,6 +3,7 @@ package com.projectswg.holocore.resources.support.global.commands.callbacks.comb import com.projectswg.common.data.encodables.tangible.PvpFaction; import com.projectswg.holocore.intents.gameplay.gcw.faction.FactionIntent; import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent; +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; @@ -19,11 +20,11 @@ public final class CmdPVP implements ICmdCallback { if (!args.isEmpty()) { if (args.contains("imperial")) { - new FactionIntent(creature, PvpFaction.IMPERIAL).broadcast(); + new FactionIntent(creature, ServerData.INSTANCE.getFactions().getFaction("imperial")).broadcast(); } else if (args.contains("rebel")) { - new FactionIntent(creature, PvpFaction.REBEL).broadcast(); + new FactionIntent(creature, ServerData.INSTANCE.getFactions().getFaction("rebel")).broadcast(); } else { - new FactionIntent(creature, PvpFaction.NEUTRAL).broadcast(); + new FactionIntent(creature, ServerData.INSTANCE.getFactions().getFaction("neutral")).broadcast(); } } else if (creature.getPvpFaction() != PvpFaction.NEUTRAL) { new FactionIntent(creature, FactionIntent.FactionIntentType.SWITCHUPDATE).broadcast(); diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java index 2416e7734..0e6759cdf 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java @@ -133,7 +133,7 @@ public class NpcCombatMode extends NpcMode { @Nullable private CreatureObject getPrimaryTarget() { return targets.stream() - .filter(creo -> creo.isEnemyOf(getAI())) + .filter(creo -> creo.isAttackable(getAI())) .filter(creo -> (creo.getPosture() != Posture.INCAPACITATED || getSpawner().isDeathblow()) && creo.getPosture() != Posture.DEAD) // Don't attack if they're already dead .min(Comparator.comparingInt(CreatureObject::getHealth)).orElse(null); } @@ -145,7 +145,7 @@ public class NpcCombatMode extends NpcMode { .filter(AIObject.class::isInstance) // get nearby AI .filter(ai -> ai.getWorldLocation().distanceTo(myLocation) < assistRange) // that can assist .map(AIObject.class::cast) - .filter(ai -> targets.stream().anyMatch(ai::isEnemyOf)) + .filter(ai -> targets.stream().anyMatch(ai::isAttackable)) .forEach(ai -> StartNpcCombatIntent.broadcast(ai, targets)); } diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java index deb8eb8d7..4f5d4ef25 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java @@ -26,7 +26,6 @@ ***********************************************************************************/ package com.projectswg.holocore.resources.support.npc.spawn; -import com.projectswg.common.data.encodables.tangible.PvpFaction; import com.projectswg.common.data.encodables.tangible.PvpFlag; import com.projectswg.common.data.encodables.tangible.PvpStatus; import com.projectswg.common.data.location.Location; @@ -34,6 +33,7 @@ import com.projectswg.common.data.location.Location.LocationBuilder; import com.projectswg.holocore.intents.gameplay.gcw.faction.FactionIntent; import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent; import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader; +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader.Faction; import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStatLoader.DetailNpcStatInfo; import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStatLoader.NpcStatInfo; import com.projectswg.holocore.resources.support.npc.ai.NpcLoiterMode; @@ -46,6 +46,7 @@ import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject; import com.projectswg.holocore.resources.support.objects.swg.tangible.OptionFlag; import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject; import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject; +import me.joshlarson.jlcommon.control.IntentChain; import me.joshlarson.jlcommon.log.Log; import me.joshlarson.jlcommon.utilities.Arguments; @@ -116,10 +117,10 @@ public class NPCCreator { private static void setFlags(AIObject object, Spawner spawner) { switch (spawner.getSpawnerFlag()) { case AGGRESSIVE: - object.setPvpFlags(PvpFlag.AGGRESSIVE); + object.setPvpFlags(PvpFlag.CAN_ATTACK_YOU); object.addOptionFlags(OptionFlag.AGGRESSIVE); case ATTACKABLE: - object.setPvpFlags(PvpFlag.ATTACKABLE); + object.setPvpFlags(PvpFlag.YOU_CAN_ATTACK); object.addOptionFlags(OptionFlag.HAM_BAR); break; case INVULNERABLE: @@ -128,19 +129,14 @@ public class NPCCreator { } } - private static void setNPCFaction(TangibleObject object, PvpFaction faction, boolean specForce) { - if (faction == PvpFaction.NEUTRAL) { - return; - } - - // Clear any existing flags that mark them as attackable - object.clearPvpFlags(PvpFlag.ATTACKABLE, PvpFlag.AGGRESSIVE); - object.removeOptionFlags(OptionFlag.AGGRESSIVE); - - new FactionIntent(object, faction).broadcast(); - + private static void setNPCFaction(TangibleObject object, Faction faction, boolean specForce) { if (specForce) { - new FactionIntent(object, PvpStatus.SPECIALFORCES).broadcast(); + IntentChain.broadcastChain( + new FactionIntent(object, faction), + new FactionIntent(object, PvpStatus.SPECIALFORCES) + ); + } else { + new FactionIntent(object, faction).broadcast(); } } diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java index b64bcb429..844bf7d03 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java @@ -26,9 +26,9 @@ ***********************************************************************************/ package com.projectswg.holocore.resources.support.npc.spawn; -import com.projectswg.common.data.encodables.tangible.PvpFaction; import com.projectswg.common.data.location.Location; import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader; +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader.Faction; import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcLoader.CreatureNpcInfo; import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcLoader.DroidNpcInfo; import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcLoader.HumanoidNpcInfo; @@ -194,7 +194,7 @@ public final class Spawner { return npc.getIffs(); } - public PvpFaction getFaction() { + public Faction getFaction() { return npc.getFaction(); } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.java b/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.java deleted file mode 100644 index 7d1ce557d..000000000 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.java +++ /dev/null @@ -1,117 +0,0 @@ -/*********************************************************************************** - * Copyright (c) 2018 /// 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.resources.support.objects.awareness; - -import com.projectswg.holocore.resources.support.objects.swg.SWGObject; -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -class TerrainMapChunk { - - private static final List EMPTY_AWARENESS = List.of(); - - private final CopyOnWriteArrayList objects; - private final CopyOnWriteArrayList creatures; - private TerrainMapChunk [] neighbors; - - public TerrainMapChunk() { - this.objects = new CopyOnWriteArrayList<>(); - this.creatures = new CopyOnWriteArrayList<>(); - this.neighbors = new TerrainMapChunk[]{this}; - } - - public void link(TerrainMapChunk neighbor) { - assert this != neighbor; - int length = neighbors.length; - neighbors = Arrays.copyOf(neighbors, length+1); - neighbors[length] = neighbor; - } - - public void addObject(@NotNull SWGObject obj) { - for (TerrainMapChunk neighbor : neighbors) - neighbor.objects.addIfAbsent(obj); - - if (obj instanceof CreatureObject && ((CreatureObject) obj).isPlayer()) - creatures.add((CreatureObject) obj); - } - - public void removeObject(@NotNull SWGObject obj) { - for (TerrainMapChunk neighbor : neighbors) - neighbor.objects.remove(obj); - - if (obj instanceof CreatureObject) - creatures.remove(obj); - } - - public void update() { - if (creatures.isEmpty()) - return; - List aware = new ArrayList<>(creatures.size()); - for (CreatureObject creature : creatures) { - if (creature.isLoggedInPlayer()) - aware.add(new CreatureAware(creature)); - else - creature.setAware(AwarenessType.OBJECT, EMPTY_AWARENESS); - } - - final CreatureAware [] awareCompiled = aware.toArray(new CreatureAware[0]); - for (SWGObject test : objects) { - for (CreatureAware creatureAware : awareCompiled) - creatureAware.test(test); - } - - aware.forEach(CreatureAware::commit); - } - - private static class CreatureAware { - - private final CreatureObject creature; - private final List aware; - - public CreatureAware(CreatureObject creature) { - this.creature = creature; - this.aware = new ArrayList<>(); - } - - public void test(SWGObject test) { - if (creature.isWithinAwarenessRange(test)) - aware.add(test); - } - - public void commit() { - creature.setAware(AwarenessType.OBJECT, aware); - creature.flushAwareness(); - } - - } - -} 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 new file mode 100644 index 000000000..9f337bf9d --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/awareness/TerrainMapChunk.kt @@ -0,0 +1,132 @@ +/*********************************************************************************** + * Copyright (c) 2018 /// 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 //www.gnu.org/licenses/>. * + */ +package com.projectswg.holocore.resources.support.objects.awareness + +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.tangible.OptionFlag +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList + +internal class TerrainMapChunk { + + private val objects: CopyOnWriteArrayList = CopyOnWriteArrayList() + private val npcObjects: CopyOnWriteArrayList = CopyOnWriteArrayList() + private val creatures: CopyOnWriteArrayList = CopyOnWriteArrayList() + private val npcs: CopyOnWriteArrayList = CopyOnWriteArrayList() + private var neighbors: Array = arrayOf(this) + + fun link(neighbor: TerrainMapChunk) { + assert(this !== neighbor) + val length = neighbors.size + neighbors = neighbors.copyOf(length + 1) + neighbors[length] = neighbor + } + + fun addObject(obj: SWGObject) { + for (neighbor in neighbors) + neighbor!!.objects.addIfAbsent(obj) + + if (obj is AIObject && !obj.hasOptionFlags(OptionFlag.INVULNERABLE)) { + for (neighbor in neighbors) + neighbor!!.npcObjects.addIfAbsent(obj) + npcs.add(NpcAware(obj)) + } else if (obj is CreatureObject && obj.isPlayer) { + for (neighbor in neighbors) + neighbor!!.npcObjects.addIfAbsent(obj) + creatures.add(CreatureAware(obj)) + } + } + + fun removeObject(obj: SWGObject) { + for (neighbor in neighbors) { + neighbor!!.objects.remove(obj) + neighbor.npcObjects.remove(obj) + } + + if (obj is CreatureObject) { + creatures.removeIf { it.creature == obj } + npcs.removeIf { it.creature == obj } + } + } + + fun update() { + if (creatures.isEmpty()) + return + + creatures.forEach { it.test(objects) } + npcs.forEach { it.test(npcObjects) } + } + + private class CreatureAware(val creature: CreatureObject) { + + private val aware = ArrayList() + + fun test(tests: List) { + aware.clear() + for (test in tests) { + if (creature.isWithinAwarenessRange(test)) { + aware.add(test) + } + } + if (aware.isEmpty()) + creature.setAware(AwarenessType.OBJECT, EMPTY_AWARENESS) + else + creature.setAware(AwarenessType.OBJECT, ArrayList(aware)) + creature.flushAwareness() + } + + } + + private class NpcAware(val creature: CreatureObject) { + + private val aware = ArrayList() + + fun test(tests: List) { + aware.clear() + for (test in tests) { + if (creature.isWithinAwarenessRange(test)) { + aware.add(test) + } + } + if (aware.isEmpty()) + creature.setAware(AwarenessType.OBJECT, EMPTY_AWARENESS) + else + creature.setAware(AwarenessType.OBJECT, ArrayList(aware)) + creature.flushAwareness() + } + + } + + companion object { + + private val EMPTY_AWARENESS = listOf() + + } + +} 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 9a03981e3..68deee89f 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 @@ -1004,6 +1004,22 @@ public abstract class SWGObject extends BaselineObject implements Comparable getPvpFlagsFor(TangibleObject observer) { - Set flags = super.getPvpFlagsFor(observer); - - if (observer instanceof CreatureObject) { - if (isDuelingPlayer((CreatureObject) observer)) { - flags.add(PvpFlag.ATTACKABLE); - flags.add(PvpFlag.ENEMY); - flags.add(PvpFlag.DUEL); - } - } - - return flags; - } - public boolean isBaselinesSent(SWGObject obj) { return awareness.isAware(obj); } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObjectAwareness.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObjectAwareness.java index e516a7b95..e87a35b26 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObjectAwareness.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/creature/CreatureObjectAwareness.java @@ -66,6 +66,53 @@ public class CreatureObjectAwareness { finalTeleportPacket.set(new DataTransformWithParent(creature.getObjectId(), 0, creature.getNextUpdateCount(), parent.getObjectId(), location, 0)); } + public synchronized void flushNoPlayer() { + Set newAware = creature.getAware(); + List create = new ArrayList<>(); + List added = new ArrayList<>(); + + // Create Deltas + for (SWGObject createCandidate : newAware) { + if (aware.contains(createCandidate)) + continue; + added.add(createCandidate); + } + getCreateList(create, added); + + // Server awareness update + for (SWGObject add : create) { // Using "create" here because it's filtered to ensure no crashes + aware.add(add); + awareIds.add(add.getObjectId()); + add.addObserver(creature); + creature.onObjectEnteredAware(add); + } + + List destroy = new ArrayList<>(); + List removed = new ArrayList<>(); + + // Create Deltas + for (SWGObject destroyCandidate : aware) { + if (newAware.contains(destroyCandidate)) + continue; + removed.add(destroyCandidate); + } + getDestroyList(destroy, removed); + + // Remove destroyed objects so that nobody tries to send a packet to us after we send the destroy + for (Iterator it = aware.iterator(); it.hasNext(); ) { + SWGObject currentAware = it.next(); + for (SWGObject remove : destroy) { // Since the "create" is filtered, aware could also have been filtered + if (isParent(currentAware, remove)) { + it.remove(); + awareIds.remove(currentAware.getObjectId()); + remove.removeObserver(creature); + creature.onObjectExitedAware(remove); + break; + } + } + } + } + public synchronized void flush(@NotNull Player target) { Set newAware = creature.getAware(); flushCreates(target, newAware); @@ -106,6 +153,7 @@ public class CreatureObjectAwareness { aware.add(add); awareIds.add(add.getObjectId()); add.addObserver(creature); + creature.onObjectEnteredAware(add); } // Hope we didn't screw anything up @@ -133,6 +181,7 @@ public class CreatureObjectAwareness { it.remove(); awareIds.remove(currentAware.getObjectId()); remove.removeObserver(creature); + creature.onObjectExitedAware(remove); break; } } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java index 88cf26536..51dd0c493 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java @@ -26,18 +26,15 @@ ***********************************************************************************/ package com.projectswg.holocore.resources.support.objects.swg.custom; -import com.projectswg.common.data.encodables.tangible.Posture; -import com.projectswg.common.data.encodables.tangible.PvpFlag; import com.projectswg.common.network.packets.swg.zone.baselines.Baseline.BaselineType; import com.projectswg.holocore.intents.support.npc.ai.ScheduleNpcModeIntent; import com.projectswg.holocore.intents.support.npc.ai.StartNpcCombatIntent; -import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader.SpawnerFlag; import com.projectswg.holocore.resources.support.npc.spawn.Spawner; import com.projectswg.holocore.resources.support.objects.ObjectCreator; 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.creature.CreatureState; -import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject; +import com.projectswg.holocore.resources.support.objects.swg.tangible.OptionFlag; import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject; import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool; import me.joshlarson.jlcommon.log.Log; @@ -81,47 +78,71 @@ public class AIObject extends CreatureObject { } @Override - public void onObjectMoveInAware(SWGObject aware) { - if (aware.getBaselineType() != BaselineType.CREO) + public void onObjectEnteredAware(SWGObject aware) { + if (aware.getBaselineType() != BaselineType.CREO || hasOptionFlags(OptionFlag.INVULNERABLE)) return; + if (((CreatureObject) aware).isStatesBitmask(CreatureState.MOUNTED_CREATURE)) { aware = aware.getSlottedObject("rider"); - if (aware == null) + if (!(aware instanceof CreatureObject)) return; } CreatureObject player = (CreatureObject) aware; - if (!player.isLoggedInPlayer()) + if (player.hasOptionFlags(OptionFlag.INVULNERABLE)) return; + + playersNearby.add(player); + if (activeMode != null) + activeMode.onPlayerEnterAware(player, flatDistanceTo(player)); + } + + @Override + public void onObjectExitedAware(SWGObject aware) { + if (aware.getBaselineType() != BaselineType.CREO) + return; + + if (((CreatureObject) aware).isStatesBitmask(CreatureState.MOUNTED_CREATURE)) { + aware = aware.getSlottedObject("rider"); + if (!(aware instanceof CreatureObject)) + return; + } + playersNearby.remove(aware); + if (activeMode != null) + activeMode.onPlayerExitAware((CreatureObject) aware); + } + + @Override + public void onObjectMoveInAware(SWGObject aware) { + if (aware.getBaselineType() != BaselineType.CREO) + return; + + if (((CreatureObject) aware).isStatesBitmask(CreatureState.MOUNTED_CREATURE)) { + aware = aware.getSlottedObject("rider"); + if (!(aware instanceof CreatureObject)) + return; + } + CreatureObject player = (CreatureObject) aware; + if (player.hasOptionFlags(OptionFlag.INVULNERABLE) || !playersNearby.contains(aware) || !isAttackable(player)) + return; + double distance = getLocation().flatDistanceTo(aware.getLocation()); - NpcMode activeMode = this.activeMode; - if (distance <= 100 && player.getPosture() != Posture.INCAPACITATED && player.getPosture() != Posture.DEAD) { - if (playersNearby.add(player)) { - if (activeMode != null) - activeMode.onPlayerEnterAware(player, distance); - } else { - if (activeMode != null) { - activeMode.onPlayerMoveInAware(player, distance); - if (getSpawner().getSpawnerFlag() == SpawnerFlag.AGGRESSIVE && distance < getSpawner().getAggressiveRadius() && isEnemyOf(player) && isLineOfSight(player)) { - StartNpcCombatIntent.broadcast(this, List.of(player)); - } - - } - } - } else { - if (playersNearby.remove(player)) { - if (activeMode != null) - activeMode.onPlayerExitAware(player); - } + double maxAggroDistance; + if (player.isLoggedInPlayer()) + maxAggroDistance = getSpawner().getAggressiveRadius(); + else if (!player.isPlayer()) + maxAggroDistance = 30; + else + maxAggroDistance = -1; // Ensures the following if-statement will fail and remove the player from the list + + if (distance <= maxAggroDistance && isLineOfSight(player)) { + StartNpcCombatIntent.broadcast(this, List.of(player)); } } @Override - public boolean isEnemyOf(TangibleObject obj) { - Posture myPosture = getPosture(); - if (myPosture == Posture.INCAPACITATED || myPosture == Posture.DEAD || !(obj instanceof CreatureObject)) - return false; - Posture theirPosture = ((CreatureObject) obj).getPosture(); - return (theirPosture != Posture.INCAPACITATED || hasPvpFlag(PvpFlag.AGGRESSIVE)) && theirPosture != Posture.DEAD && super.isEnemyOf(obj); + public boolean isWithinAwarenessRange(SWGObject target) { + assert target instanceof CreatureObject; + return isAttackable((CreatureObject) target) && flatDistanceTo(target) <= 50; } public void setSpawner(Spawner spawner) { diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/tangible/TangibleObject.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/tangible/TangibleObject.java index 94ed84920..d1339e352 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/tangible/TangibleObject.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/tangible/TangibleObject.java @@ -38,14 +38,18 @@ import com.projectswg.common.network.packets.swg.zone.baselines.Baseline.Baselin import com.projectswg.holocore.intents.gameplay.gcw.faction.FactionIntent; import com.projectswg.holocore.intents.gameplay.gcw.faction.FactionIntent.FactionIntentType; import com.projectswg.holocore.intents.support.objects.swg.DestroyObjectIntent; +import com.projectswg.holocore.resources.gameplay.combat.EnemyProcessor; import com.projectswg.holocore.resources.support.data.collections.SWGMap; import com.projectswg.holocore.resources.support.data.collections.SWGSet; +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData; +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader.Faction; import com.projectswg.holocore.resources.support.global.network.BaselineBuilder; import com.projectswg.holocore.resources.support.global.player.Player; import com.projectswg.holocore.resources.support.objects.permissions.ContainerResult; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.*; @@ -58,7 +62,7 @@ public class TangibleObject extends SWGObject { private int condition = 0; private Set pvpFlags = EnumSet.noneOf(PvpFlag.class); private PvpStatus pvpStatus = PvpStatus.COMBATANT; - private PvpFaction pvpFaction = PvpFaction.NEUTRAL; + private Faction faction = null; private boolean visibleGmOnly = true; private byte [] objectEffects = new byte[0]; private int optionFlags = 0; @@ -185,13 +189,22 @@ public class TangibleObject extends SWGObject { sendDelta(3, 5, pvpStatus.getValue()); } - public PvpFaction getPvpFaction() { - return pvpFaction; + @Nullable + public Faction getFaction() { + return faction; } - public void setPvpFaction(PvpFaction pvpFaction) { - this.pvpFaction = pvpFaction; + public PvpFaction getPvpFaction() { + Faction faction = this.faction; + if (faction == null) + return PvpFaction.NEUTRAL; + return faction.getPvpFaction(); + } + + public void setFaction(Faction faction) { + this.faction = faction; + PvpFaction pvpFaction = faction == null ? PvpFaction.NEUTRAL : faction.getPvpFaction(); sendDelta(3, 4, pvpFaction.getCrc()); } @@ -281,6 +294,10 @@ public class TangibleObject extends SWGObject { } sendDelta(3, 8, optionFlags); } + + public boolean hasOptionFlags(OptionFlag option) { + return (optionFlags & option.getFlag()) != 0; + } public boolean hasOptionFlags(OptionFlag ... options) { for (OptionFlag option : options) { @@ -341,63 +358,19 @@ public class TangibleObject extends SWGObject { * @param otherObject * @return true if this object is an enemy of {@code otherObject} */ - public boolean isEnemyOf(TangibleObject otherObject) { - if (otherObject.hasOptionFlags(OptionFlag.INVULNERABLE)) { - return false; - } - - if (otherObject.hasPvpFlag(PvpFlag.ATTACKABLE)) { - return true; - } - - PvpFaction ourFaction = getPvpFaction(); - PvpFaction otherFaction = otherObject.getPvpFaction(); - - if (ourFaction == PvpFaction.NEUTRAL || otherFaction == PvpFaction.NEUTRAL) { - // Neutrals are always excluded from factional combat, unless they're both neutral - return ourFaction == otherFaction; - } - - // At this point, neither are neutral - - if (ourFaction == otherFaction) { - // Members of the same faction are not enemies - return false; - } - - // At this point, they're members of opposing factions - - PvpStatus ourStatus = getPvpStatus(); - PvpStatus otherStatus = otherObject.getPvpStatus(); - - if (ourStatus == PvpStatus.ONLEAVE || otherStatus == PvpStatus.ONLEAVE) { - // They're of opposing factions, but one of them on leave - return false; - } - - // At this point, they're both either combatant or special forces - - boolean ourPlayer = getSlottedObject("ghost") != null; - boolean otherPlayer = otherObject.getSlottedObject("ghost") != null; - - if (ourPlayer && otherPlayer) { - // Two players can only attack each other if both are Special Forces - return ourStatus == PvpStatus.SPECIALFORCES && otherStatus == PvpStatus.SPECIALFORCES; - } else { - // At this point, we're dealing with player vs npc or npc vs npc - // In this case, they just need to not be on leave and we've already established this - return true; - } + public boolean isAttackable(TangibleObject otherObject) { + return EnemyProcessor.INSTANCE.isAttackable(this, otherObject); } public Set getPvpFlagsFor(TangibleObject observer) { - Set pvpFlags = EnumSet.copyOf(observer.pvpFlags); // More efficient behind the scenes + Set pvpFlags = EnumSet.noneOf(PvpFlag.class); // More efficient behind the scenes - if (isEnemyOf(observer) && getPvpFaction() != PvpFaction.NEUTRAL && observer.getPvpFaction() != PvpFaction.NEUTRAL) { - pvpFlags.add(PvpFlag.AGGRESSIVE); - pvpFlags.add(PvpFlag.ATTACKABLE); - pvpFlags.add(PvpFlag.ENEMY); - } + if (isAttackable(observer)) + pvpFlags.add(PvpFlag.YOU_CAN_ATTACK); + if (observer.isAttackable(this)) + pvpFlags.add(PvpFlag.CAN_ATTACK_YOU); + if (observer instanceof CreatureObject && ((CreatureObject) observer).isPlayer()) + pvpFlags.add(PvpFlag.PLAYER); return pvpFlags; } @@ -413,7 +386,7 @@ public class TangibleObject extends SWGObject { @Override protected void createBaseline3(Player target, BaselineBuilder bb) { super.createBaseline3(target, bb); // 4 variables - BASE3 (4) - bb.addInt(pvpFaction.getCrc()); // Faction - 4 + bb.addInt(getPvpFaction().getCrc()); // Faction - 4 bb.addInt(pvpStatus.getValue()); // Faction Status - 5 bb.addObject(appearanceData); // - 6 bb.addInt(0); // Component customization (Set, Integer) - 7 @@ -445,7 +418,7 @@ public class TangibleObject extends SWGObject { @Override protected void parseBaseline3(NetBuffer buffer) { super.parseBaseline3(buffer); - pvpFaction = PvpFaction.getFactionForCrc(buffer.getInt()); + faction = ServerData.INSTANCE.getFactions().getFaction(PvpFaction.getFactionForCrc(buffer.getInt()).name().toLowerCase(Locale.US)); pvpStatus = PvpStatus.getStatusForValue(buffer.getInt()); appearanceData.decode(buffer); SWGSet.getSwgSet(buffer, 3, 7, Integer.class); @@ -477,7 +450,8 @@ public class TangibleObject extends SWGObject { stream.addInt(condition); stream.addInt(pvpFlags.stream().mapToInt(PvpFlag::getBitmask).reduce(0, (a, b) -> a | b)); stream.addAscii(pvpStatus.name()); - stream.addAscii(pvpFaction.name()); + Faction faction = this.faction; + stream.addAscii(faction == null ? "neutral" : faction.getName()); stream.addBoolean(visibleGmOnly); stream.addArray(objectEffects); stream.addInt(optionFlags); @@ -499,7 +473,7 @@ public class TangibleObject extends SWGObject { condition = stream.getInt(); pvpFlags = PvpFlag.getFlags(stream.getInt()); pvpStatus = PvpStatus.valueOf(stream.getAscii()); - pvpFaction = PvpFaction.valueOf(stream.getAscii()); + faction = ServerData.INSTANCE.getFactions().getFaction(stream.getAscii().toLowerCase(Locale.US)); visibleGmOnly = stream.getBoolean(); objectEffects = stream.getArray(); optionFlags = stream.getInt(); @@ -515,7 +489,8 @@ public class TangibleObject extends SWGObject { data.putInteger("condition", condition); data.putInteger("pvpFlags", pvpFlags.stream().mapToInt(PvpFlag::getBitmask).reduce(0, (a, b) -> a | b)); data.putString("pvpStatus", pvpStatus.name()); - data.putString("pvpFaction", pvpFaction.name()); + Faction faction = this.faction; + data.putString("faction", faction == null ? "neutral" : faction.getName()); data.putBoolean("visibleGmOnly", visibleGmOnly); data.putByteArray("objectEffects", objectEffects); data.putInteger("optionFlags", optionFlags); @@ -531,7 +506,7 @@ public class TangibleObject extends SWGObject { condition = data.getInteger("condition", 0); pvpFlags.addAll(PvpFlag.getFlags(data.getInteger("pvpFlags", 0))); pvpStatus = PvpStatus.valueOf(data.getString("pvpStatus", "COMBATANT")); - pvpFaction = PvpFaction.valueOf(data.getString("pvpFaction", "NEUTRAL")); + faction = ServerData.INSTANCE.getFactions().getFaction(data.getString(data.containsKey("pvpFaction") ? "pvpFaction" : "faction", "neutral")); visibleGmOnly = data.getBoolean("visibleGmOnly", false); objectEffects = data.getByteArray("objectEffects"); optionFlags = data.getInteger("optionFlags", 0); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java index 2f52b2011..a4792a991 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java @@ -56,7 +56,7 @@ public class CombatDeathblowService extends Service { } // They must be enemies - if (!corpse.isEnemyOf(killer)) { + if (!corpse.isAttackable(killer)) { return; } diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.java deleted file mode 100644 index 44419b376..000000000 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.projectswg.holocore.services.gameplay.combat; - -import com.projectswg.holocore.intents.gameplay.combat.EnterCombatIntent; -import com.projectswg.holocore.intents.gameplay.combat.ExitCombatIntent; -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; -import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup; -import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool; -import me.joshlarson.jlcommon.control.IntentHandler; -import me.joshlarson.jlcommon.control.Service; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class CombatStatusService extends Service { - - private final Set inCombat; - private final ScheduledThreadPool executor; - - public CombatStatusService() { - this.inCombat = ConcurrentHashMap.newKeySet(); - this.executor = new ScheduledThreadPool(1, 3, "combat-status-service"); - } - - @Override - public boolean start() { - executor.start(); - executor.executeWithFixedRate(1000, 1000, this::periodicCombatStatusChecks); - return true; - } - - @Override - public boolean stop() { - executor.stop(); - executor.awaitTermination(1000); - return true; - } - - private void periodicCombatStatusChecks() { - for (CreatureObject creature : inCombat) { - if (creature.getTimeSinceLastCombat() >= 10E3) - ExitCombatIntent.broadcast(creature); - } - } - - @IntentHandler - private void handleEnterCombatIntent(EnterCombatIntent eci) { - CreatureObject source = eci.getSource(); - CreatureObject target = eci.getTarget(); - if (source.isInCombat()) - return; - - source.setInCombat(true); - target.addDefender(source); - source.addDefender(target); - inCombat.add(source); - inCombat.add(target); - } - - @IntentHandler - private void handleExitCombatIntent(ExitCombatIntent eci) { - CreatureObject source = eci.getSource(); - List defenders = source.getDefenders().stream().map(ObjectLookup::getObjectById).map(CreatureObject.class::cast).collect(Collectors.toList()); - source.clearDefenders(); - for (CreatureObject defender : defenders) { - defender.removeDefender(source); - if (!defender.hasDefenders()) - ExitCombatIntent.broadcast(defender); - } - if (source.hasDefenders()) - return; - - source.setInCombat(false); - inCombat.remove(source); - } -} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.kt new file mode 100644 index 000000000..0237b7d69 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatStatusService.kt @@ -0,0 +1,116 @@ +package com.projectswg.holocore.services.gameplay.combat + +import com.projectswg.holocore.intents.gameplay.combat.EnterCombatIntent +import com.projectswg.holocore.intents.gameplay.combat.ExitCombatIntent +import com.projectswg.holocore.intents.gameplay.combat.duel.DuelPlayerIntent +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject +import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup +import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool +import me.joshlarson.jlcommon.control.IntentHandler +import me.joshlarson.jlcommon.control.Service +import java.util.concurrent.ConcurrentHashMap +import java.util.stream.Collectors + +class CombatStatusService : Service() { + + private val inCombat: MutableSet + private val duels: MutableSet + private val executor: ScheduledThreadPool + + init { + this.inCombat = ConcurrentHashMap.newKeySet() + this.duels = ConcurrentHashMap.newKeySet() + this.executor = ScheduledThreadPool(1, 3, "combat-status-service") + } + + override fun start(): Boolean { + executor.start() + executor.executeWithFixedRate(1000, 1000) { this.periodicCombatStatusChecks() } + return true + } + + override fun stop(): Boolean { + executor.stop() + executor.awaitTermination(1000) + return true + } + + private fun periodicCombatStatusChecks() { + for (creature in inCombat) { + if (creature.timeSinceLastCombat >= 10E3 && !isDueling(creature)) + ExitCombatIntent.broadcast(creature) + } + } + + @IntentHandler + private fun handleEnterCombatIntent(eci: EnterCombatIntent) { + val source = eci.source + val target = eci.target + if (source.isInCombat) + return + + source.isInCombat = true + target.addDefender(source) + source.addDefender(target) + inCombat.add(source) + inCombat.add(target) + } + + @IntentHandler + private fun handleExitCombatIntent(eci: ExitCombatIntent) { + val source = eci.source + val defenders = source.defenders.stream().map { ObjectLookup.getObjectById(it) }.map { CreatureObject::class.java.cast(it) }.collect(Collectors.toList()) + source.clearDefenders() + for (defender in defenders) { + defender.removeDefender(source) + if (!defender.hasDefenders()) + ExitCombatIntent.broadcast(defender) + } + endDuels(source) + if (source.hasDefenders()) + return + + source.isInCombat = false + inCombat.remove(source) + } + + @IntentHandler + private fun handleDuelPlayerIntent(dpi: DuelPlayerIntent) { + when (dpi.eventType) { + DuelPlayerIntent.DuelEventType.BEGINDUEL -> { + val duel = if (dpi.sender.objectId < dpi.reciever.objectId) DuelInstance(dpi.sender, dpi.reciever) else DuelInstance(dpi.reciever, dpi.sender) + if (duels.add(duel)) { + EnterCombatIntent.broadcast(duel.playerA, duel.playerB) + EnterCombatIntent.broadcast(duel.playerB, duel.playerA) + } + } + DuelPlayerIntent.DuelEventType.END -> { + duels.remove(if (dpi.sender.objectId < dpi.reciever.objectId) DuelInstance(dpi.sender, dpi.reciever) else DuelInstance(dpi.reciever, dpi.sender)) + } + else -> {} + } + } + + private fun isDueling(player: CreatureObject): Boolean { + for (duel in duels) { + if ((duel.playerA == player || duel.playerB == player) && duel.isDueling) + return true + } + return false + } + + private fun endDuels(player: CreatureObject) { + for (duel in duels) { + if (duel.playerA == player || duel.playerB == player) { + DuelPlayerIntent(duel.playerA, duel.playerB, DuelPlayerIntent.DuelEventType.END).broadcast() + } + } + } + + data class DuelInstance(val playerA: CreatureObject, val playerB: CreatureObject) { + + val isDueling: Boolean + get() = playerA.isDuelingPlayer(playerB) + + } +} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java index 8cfab687e..3f9449c82 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java @@ -34,10 +34,7 @@ import com.projectswg.common.data.encodables.oob.StringId; import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText; import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText.Scale; import com.projectswg.common.network.packets.swg.zone.object_controller.combat.CombatAction; -import com.projectswg.common.network.packets.swg.zone.object_controller.combat.CombatAction.Defender; import com.projectswg.common.network.packets.swg.zone.object_controller.combat.CombatSpam; -import com.projectswg.holocore.intents.gameplay.combat.EnterCombatIntent; -import com.projectswg.holocore.intents.gameplay.combat.RequestCreatureDeathIntent; import com.projectswg.holocore.intents.gameplay.combat.buffs.BuffIntent; import com.projectswg.holocore.resources.support.global.commands.CombatCommand; import com.projectswg.holocore.resources.support.global.commands.Command; @@ -46,11 +43,7 @@ import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureOb import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject; import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; public class CombatCommandCommon { @@ -98,7 +91,7 @@ public class CombatCommandCommon { if (!(target instanceof TangibleObject)) return CombatStatus.INVALID_TARGET; - if (!source.isEnemyOf((TangibleObject) target)) { + if (!source.isAttackable((TangibleObject) target)) { return CombatStatus.INVALID_TARGET; } diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandHeal.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandHeal.java index 35c03d794..bc51c7cfe 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandHeal.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandHeal.java @@ -40,7 +40,6 @@ import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyT import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText.Scale; import com.projectswg.common.network.packets.swg.zone.object_controller.combat.CombatAction; import com.projectswg.common.network.packets.swg.zone.object_controller.combat.CombatAction.Defender; -import com.projectswg.holocore.resources.support.data.server_info.StandardLog; import com.projectswg.holocore.resources.support.global.commands.CombatCommand; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; @@ -84,7 +83,7 @@ enum CombatCommandHeal implements CombatCommandHitType { CreatureObject creatureTarget = (CreatureObject) target; - if (source.isEnemyOf(creatureTarget)) { + if (source.isAttackable(creatureTarget)) { doHeal(source, source, healAmount, combatCommand); } else { doHeal(source, creatureTarget, healAmount, combatCommand); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/duel/DuelService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/duel/DuelService.java index 8b4809550..09574d54a 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/duel/DuelService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/duel/DuelService.java @@ -119,25 +119,14 @@ public class DuelService extends Service { sendSystemMessage(accepter, target, "accept_self"); sendSystemMessage(target, accepter, "accept_target"); - EnterCombatIntent.broadcast(accepter, target); - EnterCombatIntent.broadcast(target, accepter); - - FactionIntent.broadcastUpdateFlags(accepter); - FactionIntent.broadcastUpdateFlags(target); - new DuelPlayerIntent(accepter, target, DuelPlayerIntent.DuelEventType.BEGINDUEL).broadcast(); - } private void endDuel(CreatureObject ender, CreatureObject target) { ender.removePlayerFromSentDuels(target); target.removePlayerFromSentDuels(ender); - ExitCombatIntent.broadcast(ender); - ExitCombatIntent.broadcast(target); - - FactionIntent.broadcastUpdateFlags(ender); - FactionIntent.broadcastUpdateFlags(target); + new DuelPlayerIntent(ender, target, DuelPlayerIntent.DuelEventType.END).broadcast(); } private void handleEndDuel(CreatureObject ender, CreatureObject target) { diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/CivilWarService.java b/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/CivilWarService.java index cb1b19716..e5d1aabbc 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/CivilWarService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/CivilWarService.java @@ -429,7 +429,7 @@ public class CivilWarService extends Service { PlayerObject playerObject = creature.getPlayerObject(); - if (fi.getNewFaction() == PvpFaction.NEUTRAL) { + if (fi.getNewFaction() == null || fi.getNewFaction().getPvpFaction() == PvpFaction.NEUTRAL) { // They've left the imperials or rebels and must have rank removed playerObject.setCurrentGcwRank(0); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/FactionFlagService.java b/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/FactionFlagService.java index 6e85bfc17..e9ae701d3 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/FactionFlagService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/gcw/faction/FactionFlagService.java @@ -14,6 +14,7 @@ import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent; import com.projectswg.holocore.intents.support.global.zone.PlayerTransformedIntent; import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader; import com.projectswg.holocore.resources.support.data.server_info.loader.StaticPvpZoneLoader; +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader.Faction; import com.projectswg.holocore.resources.support.global.player.Player; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; import com.projectswg.holocore.resources.support.objects.swg.cell.CellObject; @@ -174,9 +175,9 @@ public class FactionFlagService extends Service { private void handleTypeChange(FactionIntent fi) { TangibleObject target = fi.getTarget(); - PvpFaction newFaction = fi.getNewFaction(); + Faction newFaction = fi.getNewFaction(); - target.setPvpFaction(newFaction); + target.setFaction(newFaction); handleFlagChange(target); } diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/world/travel/PlayerMountService.java b/src/main/java/com/projectswg/holocore/services/gameplay/world/travel/PlayerMountService.java index d5eb43b03..7c1555b52 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/world/travel/PlayerMountService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/world/travel/PlayerMountService.java @@ -240,7 +240,7 @@ public class PlayerMountService extends Service { CreatureObject mount = (CreatureObject) ObjectCreator.createObjectFromTemplate(template); mount.systemMove(null, player.getLocation()); mount.addOptionFlags(OptionFlag.MOUNT); // The mount won't appear properly if this isn't set - mount.setPvpFaction(player.getPvpFaction()); + mount.setFaction(player.getFaction()); mount.setOwnerId(player.getObjectId()); // Client crash if this isn't set before making anyone aware // TODO after combat there's a delay diff --git a/src/main/java/com/projectswg/holocore/services/support/data/dev/DeveloperService.java b/src/main/java/com/projectswg/holocore/services/support/data/dev/DeveloperService.java index 2f8f57d1a..093e5da74 100644 --- a/src/main/java/com/projectswg/holocore/services/support/data/dev/DeveloperService.java +++ b/src/main/java/com/projectswg/holocore/services/support/data/dev/DeveloperService.java @@ -55,7 +55,7 @@ public class DeveloperService extends Service { private void setupDeveloperArea() { AIObject dummy = spawnObject("object/mobile/shared_target_dummy_blacksun.iff", new Location(3500, 5, -4800, Terrain.DEV_AREA), AIObject.class); - dummy.setPvpFlags(PvpFlag.ATTACKABLE); + dummy.setPvpFlags(PvpFlag.YOU_CAN_ATTACK); } private void setupCharacterBuilders() { diff --git a/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandQueueService.java b/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandQueueService.java index 74e7f3c1a..a21e858aa 100644 --- a/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandQueueService.java +++ b/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandQueueService.java @@ -193,7 +193,7 @@ public class CommandQueueService extends Service { if (command.getTarget() == null) { target = command.getSource(); } else if (command.getTarget() instanceof CreatureObject) { - target = command.getSource().isEnemyOf((CreatureObject) command.getTarget()) ? command.getSource() : command.getTarget(); + target = command.getSource().isAttackable((CreatureObject) command.getTarget()) ? command.getSource() : command.getTarget(); } else { target = null; } diff --git a/src/test/java/com/projectswg/holocore/resources/gameplay/combat/TestEnemyProcessor.kt b/src/test/java/com/projectswg/holocore/resources/gameplay/combat/TestEnemyProcessor.kt new file mode 100644 index 000000000..f043ec53c --- /dev/null +++ b/src/test/java/com/projectswg/holocore/resources/gameplay/combat/TestEnemyProcessor.kt @@ -0,0 +1,124 @@ +/*********************************************************************************** + * 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.resources.gameplay.combat + +import com.projectswg.common.data.encodables.tangible.PvpStatus +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import com.projectswg.holocore.resources.support.data.server_info.loader.combat.FactionLoader +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.tangible.OptionFlag +import com.projectswg.holocore.test.resources.GenericCreatureObject +import com.projectswg.holocore.test.runners.TestRunnerNoIntents +import org.junit.Assert +import org.junit.Test + +class TestEnemyProcessor: TestRunnerNoIntents() { + + private val neutral = ServerData.factions.getFaction("neutral") ?: throw AssertionError("Failed to lookup 'neutral'") + private val rebel = ServerData.factions.getFaction("rebel") ?: throw AssertionError("Failed to lookup 'rebel'") + private val imperial = ServerData.factions.getFaction("imperial") ?: throw AssertionError("Failed to lookup 'imperial'") + private val gungan = ServerData.factions.getFaction("gungan") ?: throw AssertionError("Failed to lookup 'gungan'") + private val townsperson = ServerData.factions.getFaction("townsperson") ?: throw AssertionError("Failed to lookup 'townsperson'") + + @Test + fun testPvP() { + val player1 = GenericCreatureObject(1) + val player2 = GenericCreatureObject(2) + val statusList = PvpStatus.values() + val factions = listOf(neutral, rebel, imperial, gungan, townsperson) + + for (player1Faction in factions) { + for (player2Faction in factions) { + val enemies = (player1Faction == rebel && player2Faction == imperial) || (player1Faction == imperial && player2Faction == rebel) + for (player1Status in statusList) { + player1.pvpStatus = player1Status + for (player2Status in statusList) { + player2.pvpStatus = player2Status + val canAttack = enemies && player1Status == PvpStatus.SPECIALFORCES && player2Status == PvpStatus.SPECIALFORCES + testAttackable(player1, player2, player1Faction, player2Faction, playerAttackable = canAttack, npcAttackable = canAttack) + } + } + } + } + } + + @Test + fun testDuel() { + val player1 = GenericCreatureObject(1) + val player2 = GenericCreatureObject(2) + + testAttackable(player1, player2, neutral, neutral, playerAttackable = false, npcAttackable = false) + player1.addPlayerToSentDuels(player2) + player2.addPlayerToSentDuels(player1) + testAttackable(player1, player2, neutral, neutral, playerAttackable = true, npcAttackable = true) + } + + @Test + fun testPvE() { + val player = GenericCreatureObject(1) + val npc = AIObject(2) + val factions = listOf(neutral, rebel, imperial, gungan, townsperson) + + for (player1Faction in factions) { + for (player2Faction in factions) { + val enemies = (player1Faction == rebel && player2Faction == imperial) || (player1Faction == imperial && player2Faction == rebel) + testAttackable(player, npc, player1Faction, player2Faction, playerAttackable = player1Faction == neutral || player2Faction == neutral || !player1Faction.isAlly(player2Faction), npcAttackable = enemies) + } + } + + npc.addOptionFlags(OptionFlag.AGGRESSIVE) + for (player1Faction in factions) { + for (player2Faction in factions) { + val friends = player1Faction != neutral && player2Faction != neutral && player1Faction.isAlly(player2Faction) + testAttackable(player, npc, player1Faction, player2Faction, playerAttackable = !friends, npcAttackable = !friends) + } + } + } + + @Test + fun testEvE() { + val npc1 = AIObject(1) + val npc2 = AIObject(2) + val factions = listOf(neutral, rebel, imperial, gungan, townsperson) + + for (npc1Faction in factions) { + for (npc2Faction in factions) { + val enemies = (npc1Faction == rebel && npc2Faction == imperial) || (npc1Faction == imperial && npc2Faction == rebel) + testAttackable(npc1, npc2, npc1Faction, npc2Faction, playerAttackable = enemies, npcAttackable = enemies) + } + } + } + + private fun testAttackable(player: CreatureObject, npc: CreatureObject, playerFaction: FactionLoader.Faction, npcFaction: FactionLoader.Faction, playerAttackable: Boolean, npcAttackable: Boolean) { + player.faction = playerFaction; npc.faction = npcFaction + Assert.assertEquals("${player.faction} ${if (playerAttackable) "is not" else "is"} able to attack ${npc.faction}", playerAttackable, EnemyProcessor.isAttackable(player, npc)) + Assert.assertEquals("${npc.faction} ${if (npcAttackable) "is not" else "is"} able to attack ${player.faction}", npcAttackable, EnemyProcessor.isAttackable(npc, player)) + } + +} diff --git a/src/test/java/com/projectswg/holocore/resources/support/objects/swg/TestSWGPersistence.java b/src/test/java/com/projectswg/holocore/resources/support/objects/swg/TestSWGPersistence.java index ed42dc29d..6c214d8cc 100644 --- a/src/test/java/com/projectswg/holocore/resources/support/objects/swg/TestSWGPersistence.java +++ b/src/test/java/com/projectswg/holocore/resources/support/objects/swg/TestSWGPersistence.java @@ -143,7 +143,7 @@ public class TestSWGPersistence { "condition", obj.getCondition(), "pvpFlags", obj.getPvpFlags().stream().mapToInt(PvpFlag::getBitmask).reduce(0, (a, b) -> a | b), "pvpStatus", obj.getPvpStatus().name(), - "pvpFaction", obj.getPvpFaction().name(), + "faction", obj.getFaction() == null ? "neutral" : obj.getFaction().getName(), "visibleGmOnly", obj.isVisibleGmOnly(), "objectEffects", new Binary(obj.getObjectEffects()), "optionFlags", obj.getOptionFlags().stream().map(OptionFlag::getFlag).reduce(0, (a, b) -> a | b) diff --git a/src/test/java/com/projectswg/holocore/test/resources/GenericPlayer.java b/src/test/java/com/projectswg/holocore/test/resources/GenericPlayer.java index 3d02e2515..277dc2840 100644 --- a/src/test/java/com/projectswg/holocore/test/resources/GenericPlayer.java +++ b/src/test/java/com/projectswg/holocore/test/resources/GenericPlayer.java @@ -39,6 +39,7 @@ import com.projectswg.common.network.packets.swg.zone.insertion.CmdStartScene; import com.projectswg.common.network.packets.swg.zone.object_controller.DataTransform; import com.projectswg.common.network.packets.swg.zone.object_controller.DataTransformWithParent; import com.projectswg.common.network.packets.swg.zone.object_controller.ObjectController; +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData; import com.projectswg.holocore.resources.support.global.player.Player; import com.projectswg.holocore.resources.support.objects.ObjectCreator; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; @@ -275,7 +276,7 @@ public class GenericPlayer extends Player { SWGObject obj = objects.get(p.getObjectId()); assertNotNull(obj); assertTrue(obj instanceof CreatureObject); - ((CreatureObject) obj).setPvpFaction(p.getPlayerFaction()); + ((CreatureObject) obj).setFaction(ServerData.INSTANCE.getFactions().getFaction(p.getPlayerFaction().name().toLowerCase(Locale.US))); ((CreatureObject) obj).setPvpFlags(p.getPvpFlags()); }