Fixed dueling and added NPC-NPC combat

This commit is contained in:
Obique
2019-09-01 16:51:28 -04:00
parent eb2993c121
commit c7fe51c96b
31 changed files with 786 additions and 417 deletions

View File

@@ -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();
}

View File

@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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
}
}
}

View File

@@ -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

View File

@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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<String, Faction> = 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<String> {
if (listStr == "-" || listStr.isBlank())
return emptyList()
return listStr.toLowerCase(Locale.US).split(';')
}
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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));
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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 <http://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 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<SWGObject> EMPTY_AWARENESS = List.of();
private final CopyOnWriteArrayList<SWGObject> objects;
private final CopyOnWriteArrayList<CreatureObject> 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<CreatureAware> 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<SWGObject> 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();
}
}
}

View File

@@ -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 <http:></http:>//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<SWGObject> = CopyOnWriteArrayList()
private val npcObjects: CopyOnWriteArrayList<SWGObject> = CopyOnWriteArrayList()
private val creatures: CopyOnWriteArrayList<CreatureAware> = CopyOnWriteArrayList()
private val npcs: CopyOnWriteArrayList<NpcAware> = CopyOnWriteArrayList()
private var neighbors: Array<TerrainMapChunk?> = 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<SWGObject>()
fun test(tests: List<SWGObject>) {
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<SWGObject>()
fun test(tests: List<SWGObject>) {
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<SWGObject>()
}
}

View File

@@ -1004,6 +1004,22 @@ public abstract class SWGObject extends BaselineObject implements Comparable<SWG
return getAware(AwarenessType.OBJECT);
}
/**
* Called when an object has entered this object's awareness
* @param aware the object that has entered awareness
*/
public void onObjectEnteredAware(SWGObject aware) {
}
/**
* Called when an object has exited this object's awareness
* @param aware the object that has entered awareness
*/
public void onObjectExitedAware(SWGObject aware) {
}
/**
* Called when this object has been moved somehow via awareness. This will
* not run on any buildout or snapshot object!

View File

@@ -30,7 +30,6 @@ import com.projectswg.common.data.CRC;
import com.projectswg.common.data.HologramColour;
import com.projectswg.common.data.encodables.mongo.MongoData;
import com.projectswg.common.data.encodables.tangible.Posture;
import com.projectswg.common.data.encodables.tangible.PvpFlag;
import com.projectswg.common.data.encodables.tangible.Race;
import com.projectswg.common.data.location.Location;
import com.projectswg.common.data.location.Terrain;
@@ -101,10 +100,12 @@ public class CreatureObject extends TangibleObject {
public void flushAwareness() {
Player owner = getOwnerShallow();
if (getTerrain() == Terrain.GONE || owner == null || owner.getPlayerState() == PlayerState.DISCONNECTED)
return;
awareness.flush(owner);
sendAndFlushAllDeltas();
if (getTerrain() == Terrain.GONE || owner == null || owner.getPlayerState() == PlayerState.DISCONNECTED) {
awareness.flushNoPlayer();
} else {
awareness.flush(owner);
sendAndFlushAllDeltas();
}
}
public void resetObjectsAware() {
@@ -209,7 +210,7 @@ public class CreatureObject extends TangibleObject {
getAllChildren(children, obj);
}
public final boolean isWithinAwarenessRange(SWGObject target) {
public boolean isWithinAwarenessRange(SWGObject target) {
assert isPlayer();
Player owner = getOwnerShallow();
@@ -957,12 +958,6 @@ public class CreatureObject extends TangibleObject {
damageMap.put(attacker, damage);
}
}
public boolean isAttackable(CreatureObject otherObject) {
Posture otherPosture = otherObject.getPosture();
return isEnemyOf(otherObject) && otherPosture != Posture.INCAPACITATED && otherPosture != Posture.DEAD;
}
public boolean hasSentDuelRequestToPlayer(CreatureObject player) {
return sentDuels.contains(player);
@@ -980,46 +975,6 @@ public class CreatureObject extends TangibleObject {
sentDuels.remove(player);
}
/**
* Members of the same faction might be enemies, if there are players
* involved.
* @param otherObject
* @return
*/
@Override
public boolean isEnemyOf(TangibleObject otherObject) {
if (!(otherObject instanceof CreatureObject)) {
// If the other object isn't a creature, then our job here's done
return super.isEnemyOf(otherObject);
}
// TODO bounty hunting
// TODO pets, vehicles etc having same flagging as their owner
// TODO guild wars
if (isDuelingPlayer((CreatureObject)otherObject)) {
// Dueling is an exception, since it allows allies to be enemies
return true;
}
return super.isEnemyOf(otherObject); // Default
}
@Override
public Set<PvpFlag> getPvpFlagsFor(TangibleObject observer) {
Set<PvpFlag> 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);
}

View File

@@ -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<SWGObject> newAware = creature.getAware();
List<SWGObject> create = new ArrayList<>();
List<SWGObject> 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<SWGObject> destroy = new ArrayList<>();
List<SWGObject> 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<SWGObject> 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<SWGObject> 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;
}
}

View File

@@ -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) {

View File

@@ -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<PvpFlag> 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<PvpFlag> getPvpFlagsFor(TangibleObject observer) {
Set<PvpFlag> pvpFlags = EnumSet.copyOf(observer.pvpFlags); // More efficient behind the scenes
Set<PvpFlag> 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);

View File

@@ -56,7 +56,7 @@ public class CombatDeathblowService extends Service {
}
// They must be enemies
if (!corpse.isEnemyOf(killer)) {
if (!corpse.isAttackable(killer)) {
return;
}

View File

@@ -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<CreatureObject> 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<CreatureObject> 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);
}
}

View File

@@ -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<CreatureObject>
private val duels: MutableSet<DuelInstance>
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)
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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))
}
}

View File

@@ -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)

View File

@@ -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());
}