From 175a3f1ff5c5375e94127cdd9cf9fc4e7d4ebccf Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sun, 18 Aug 2024 11:33:52 -0500 Subject: [PATCH 01/20] refactor: Add function for streaming bay weapons WeaponMounted.getBayWeapons() iterates through the entire bay before returning, which isn't always necessary. It has been refactored to allow for the stream to be used directly. --- .../megamek/common/equipment/WeaponMounted.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/common/equipment/WeaponMounted.java b/megamek/src/megamek/common/equipment/WeaponMounted.java index 48b39f59bd1..ca685cc00d4 100644 --- a/megamek/src/megamek/common/equipment/WeaponMounted.java +++ b/megamek/src/megamek/common/equipment/WeaponMounted.java @@ -30,6 +30,7 @@ import java.util.Objects; import java.util.Vector; import java.util.stream.Collectors; +import java.util.stream.Stream; public class WeaponMounted extends Mounted { @@ -207,12 +208,19 @@ public void clearBayWeapons() { } /** - * @return All the weapon mounts in the bay. + * @return A stream containing the weapon mounts in the bay. */ - public List getBayWeapons() { + public Stream streamBayWeapons() { return bayWeapons.stream() .map(i -> getEntity().getWeapon(i)) - .filter(Objects::nonNull) + .filter(Objects::nonNull); + } + + /** + * @return All the weapon mounts in the bay. + */ + public List getBayWeapons() { + return streamBayWeapons() .collect(Collectors.toList()); } From 2db87957d8e0150882026c42d7b6a04389765a2e Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sun, 18 Aug 2024 13:39:05 -0500 Subject: [PATCH 02/20] refactor: Use stream composition for finding TAG spotters --- .../megamek/common/actions/WeaponAttackAction.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 19a137259b1..1414fc441e0 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -555,11 +555,12 @@ private static ToHitData toHitCalc(Game game, int attackerId, Targetable target, || (atype.getAmmoType() == AmmoType.T_NLRM) || (atype.getAmmoType() == AmmoType.T_MEK_MORTAR)) && (munition.contains(AmmoType.Munitions.M_SEMIGUIDED))) { - for (TagInfo ti : game.getTagInfo()) { - if (target.getId() == ti.target.getId()) { - spotter = game.getEntity(ti.attackerId); - } - } + final Targetable currentTarget = target; // Required for concurrency reasons + spotter = game.getTagInfo().stream() + .filter(ti -> currentTarget.getId() == ti.target.getId()) + .findAny() + .map(ti -> game.getEntity(ti.attackerId)) + .orElse(null); } } From be1cec77567b3afaa40125a3d58c50c3ea7cd9fd Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sun, 18 Aug 2024 13:40:03 -0500 Subject: [PATCH 03/20] refactor: Use stream composition for NC3 firing solutions --- .../common/actions/WeaponAttackAction.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 1414fc441e0..0a12535d53e 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -955,11 +955,8 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta toHit = new ToHitData(); } - Entity te = null; - if (ttype == Targetable.TYPE_ENTITY) { - //Some weapons only target valid entities - te = (Entity) target; - } + //Some weapons only target valid entities + final Entity te = ttype == Targetable.TYPE_ENTITY ? (Entity) target : null; // If the attacker and target are in the same building & hex, they can // always attack each other, TW pg 175. @@ -1276,13 +1273,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && ae.isSpaceborne()) { boolean networkFiringSolution = false; //Check to see if the attacker has a firing solution. Naval C3 networks share targeting data - if (ae.hasNavalC3()) { - for (Entity en : game.getC3NetworkMembers(ae)) { - if (te != null && en.hasFiringSolutionFor(te.getId())) { - networkFiringSolution = true; - break; - } - } + if (ae.hasNavalC3() && te != null + && game.getC3NetworkMembers(ae).stream().anyMatch(en -> en.hasFiringSolutionFor(te.getId())) { + networkFiringSolution = true; } if (!networkFiringSolution) { //If we don't check for target type here, we can't fire screens and missiles at hexes... From 29643a56e2c9bda9d5b9e2fc58349ed29d12d10c Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sun, 18 Aug 2024 13:45:33 -0500 Subject: [PATCH 04/20] refactor: Use stream composition for C3 firing solutions --- .../common/actions/WeaponAttackAction.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 0a12535d53e..260e2288713 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1274,7 +1274,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta boolean networkFiringSolution = false; //Check to see if the attacker has a firing solution. Naval C3 networks share targeting data if (ae.hasNavalC3() && te != null - && game.getC3NetworkMembers(ae).stream().anyMatch(en -> en.hasFiringSolutionFor(te.getId())) { + && game.getC3NetworkMembers(ae).stream().anyMatch(en -> en.hasFiringSolutionFor(te.getId()))) { networkFiringSolution = true; } if (!networkFiringSolution) { @@ -1297,15 +1297,14 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && (te != null) && te.hasSeenEntity(ae.getOwner())) && !isArtilleryIndirect && !isIndirect && !isBearingsOnlyMissile) { boolean networkSee = false; - if (ae.hasC3() || ae.hasC3i() || ae.hasActiveNovaCEWS()) { + if (ae.hasC3() || ae.hasC3i() || ae.hasActiveNovaCEWS() + && game.getEntitiesVector().stream().anyMatch(en -> + !en.isEnemyOf(ae) + && en.onSameC3NetworkAs(ae) + && Compute.canSee(game, en, target))) { // c3 units can fire if any other unit in their network is in // visual or sensor range - for (Entity en : game.getEntitiesVector()) { - if (!en.isEnemyOf(ae) && en.onSameC3NetworkAs(ae) && Compute.canSee(game, en, target)) { - networkSee = true; - break; - } - } + networkSee = true; } if (!networkSee) { if (!Compute.inSensorRange(game, ae, target, null)) { From 34ecf28aa00fba046bcca80fa391a0193faa62be Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sun, 18 Aug 2024 14:59:34 -0500 Subject: [PATCH 05/20] refactor: Use stream composition to check for usable weapon bays --- .../common/actions/WeaponAttackAction.java | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 260e2288713..c4082fcba61 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1403,27 +1403,17 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // limit large craft to zero net heat and to heat by arc final int heatCapacity = ae.getHeatCapacity(); - if (ae.usesWeaponBays() && (weapon != null) && !weapon.getBayWeapons().isEmpty()) { + if (ae.usesWeaponBays() && (weapon != null)) { int totalHeat = 0; // first check to see if there are any usable weapons - boolean usable = false; - for (WeaponMounted m : weapon.getBayWeapons()) { - WeaponType bayWType = m.getType(); - boolean bayWUsesAmmo = (bayWType.getAmmoType() != AmmoType.T_NA); - if (m.canFire()) { - if (bayWUsesAmmo) { - if ((m.getLinked() != null) && (m.getLinked().getUsableShotsLeft() > 0)) { - usable = true; - break; - } - } else { - usable = true; - break; - } - } - } - if (!usable) { + if (!weapon.streamBayWeapons() + .filter(WeaponMounted::canFire) + .anyMatch(m -> + m.getType().getAmmoType() == AmmoType.T_NA + || Optional.ofNullable(m.getLinked()).map(a -> a.getUsableShotsLeft() > 0).orElse(false) + ) + ) { return Messages.getString("WeaponAttackAction.BayNotReady"); } From 80404a081906abd13fc1ecc06467a47a4c14a57e Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Tue, 20 Aug 2024 08:19:07 -0500 Subject: [PATCH 06/20] refactor: Compact Naval C3 firing solution check Another contributor pointed out that networkFiringSolution wasn't actually necessary, since it was checked immediately and then never again. --- .../src/megamek/common/actions/WeaponAttackAction.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index c4082fcba61..0d7f5eee890 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1271,13 +1271,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_STRATOPS_ADVANCED_SENSORS) && game.getOptions().booleanOption(OptionsConstants.ADVANCED_DOUBLE_BLIND) && ae.isSpaceborne()) { - boolean networkFiringSolution = false; //Check to see if the attacker has a firing solution. Naval C3 networks share targeting data - if (ae.hasNavalC3() && te != null - && game.getC3NetworkMembers(ae).stream().anyMatch(en -> en.hasFiringSolutionFor(te.getId()))) { - networkFiringSolution = true; - } - if (!networkFiringSolution) { + if (!ae.hasNavalC3() || te == null + || game.getC3NetworkMembers(ae).stream().noneMatch(en -> en.hasFiringSolutionFor(te.getId()))) { //If we don't check for target type here, we can't fire screens and missiles at hexes... if (target.getTargetType() == Targetable.TYPE_ENTITY && (te != null && !ae.hasFiringSolutionFor(te.getId()))) { return Messages.getString("WeaponAttackAction.NoFiringSolution"); From d3c98a9958292c88f86e77d5ede240e3732e17d5 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Tue, 20 Aug 2024 09:21:32 -0500 Subject: [PATCH 07/20] refactor: Use stream composition for adding up bay heat The relevant code has been heavily restructured to eliminate unnecessary calculations and memory allocation. In the process of doing so, it became apparent that MegaMek is not implementing the penalties of firing an overheating arc as mandated by Total Warfare. A comment to that effect has been added. --- .../common/actions/WeaponAttackAction.java | 114 ++++++------------ 1 file changed, 39 insertions(+), 75 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 0d7f5eee890..986638485c2 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -40,6 +40,8 @@ import java.io.Serializable; import java.util.*; +import java.util.stream.Stream; +import java.util.stream.Collectors; /** * Represents intention to fire a weapon at the target. @@ -1400,8 +1402,6 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // limit large craft to zero net heat and to heat by arc final int heatCapacity = ae.getHeatCapacity(); if (ae.usesWeaponBays() && (weapon != null)) { - int totalHeat = 0; - // first check to see if there are any usable weapons if (!weapon.streamBayWeapons() .filter(WeaponMounted::canFire) @@ -1413,85 +1413,49 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.BayNotReady"); } - // create an array of booleans of locations - boolean[] usedFrontArc = new boolean[ae.locations()]; - boolean[] usedRearArc = new boolean[ae.locations()]; - for (int i = 0; i < ae.locations(); i++) { - usedFrontArc[i] = false; - usedRearArc[i] = false; - } - - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - Object o = i.nextElement(); - if (!(o instanceof WeaponAttackAction)) { - continue; - } - WeaponAttackAction prevAttack = (WeaponAttackAction) o; - // Strafing attacks only count heat for first shot - if (prevAttack.isStrafing() && !prevAttack.isStrafingFirstShot()) { - continue; - } - if ((prevAttack.getEntityId() == attackerId) && (weaponId != prevAttack.getWeaponId())) { - WeaponMounted prevWeapon = (WeaponMounted) ae.getEquipment(prevAttack.getWeaponId()); - if (prevWeapon != null) { - int loc = prevWeapon.getLocation(); - boolean rearMount = prevWeapon.isRearMounted(); - if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_HEAT_BY_BAY)) { - for (WeaponMounted bWeapon : prevWeapon.getBayWeapons()) { - totalHeat += bWeapon.getCurrentHeat(); - } - } else { - if (!rearMount) { - if (!usedFrontArc[loc]) { - totalHeat += ae.getHeatInArc(loc, rearMount); - usedFrontArc[loc] = true; - } - } else { - if (!usedRearArc[loc]) { - totalHeat += ae.getHeatInArc(loc, rearMount); - usedRearArc[loc] = true; - } - } - } - } - } - } + // A lazy stream that evaluates to the weapon bays that have already been fired. + Stream firedWeaponBays = game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .filter(prevAttack -> + // Strafing attacks only count heat for first shot + (!prevAttack.isStrafing() || prevAttack.isStrafingFirstShot()) + && prevAttack.getEntityId() == attackerId + ) + .map(WeaponAttackAction::getWeaponId) + .filter(id -> id != weaponId) + .flatMap(id -> Stream.ofNullable((WeaponMounted) ae.getEquipment(id))); - // now check the current heat - int loc = weapon.getLocation(); - boolean rearMount = weapon.isRearMounted(); - int currentHeat = ae.getHeatInArc(loc, rearMount); if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_HEAT_BY_BAY)) { - currentHeat = 0; - for (WeaponMounted bWeapon : weapon.getBayWeapons()) { - currentHeat += bWeapon.getCurrentHeat(); - } - } - // check to see if this is currently the only arc being fired - boolean onlyArc = true; - for (int nLoc = 0; nLoc < ae.locations(); nLoc++) { - if (nLoc == loc) { - continue; - } - if (usedFrontArc[nLoc] || usedRearArc[nLoc]) { - onlyArc = false; - break; - } - } + // Total heat of fired weapon bays and current weapon bay + int totalHeat = Stream.concat(firedWeaponBays, Stream.of(weapon)) + .flatMap(WeaponMounted::streamBayWeapons) + .collect(Collectors.summingInt(WeaponMounted::getCurrentHeat)); - if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_HEAT_BY_BAY)) { - if ((totalHeat + currentHeat) > heatCapacity) { - // FIXME: This is causing weird problems (try firing all the - // Suffen's nose weapons) + if (totalHeat > heatCapacity) { return Messages.getString("WeaponAttackAction.HeatOverCap"); } } else { - if (!rearMount) { - if (!usedFrontArc[loc] && ((totalHeat + currentHeat) > heatCapacity) && !onlyArc) { - return Messages.getString("WeaponAttackAction.HeatOverCap"); - } - } else { - if (!usedRearArc[loc] && ((totalHeat + currentHeat) > heatCapacity) && !onlyArc) { + // A map of arcs that have already been fired, keyed by location and whether an arc is rear-mounted. + Map> firedArcs = firedWeaponBays.collect( + Collectors.groupingBy(WeaponMounted::getLocation, + Collectors.mapping(WeaponMounted::isRearMounted, Collectors.toSet()) + ) + ); + + // If a weapon in the same arc has already been fired, no additional heat generation will be incurred + if (!firedArcs.get(weapon.getLocation()).contains(weapon.isRearMounted())) { + // Add up heat from each firing arc + int totalHeat = firedArcs.entrySet().stream() + .flatMapToInt(a -> a.getValue().stream().mapToInt(direction -> ae.getHeatInArc(a.getKey(), direction))) + .sum(); + + int currentHeat = ae.getHeatInArc(weapon.getLocation(), weapon.isRearMounted()); + + // If no other arcs have been fired, an arc may be fired even if it exceeds the heat capacity of the unit. + + // TODO: Add Control Roll for firing single overheating arc as per page 161 of the tenth printing of Total Warfare + if (totalHeat + currentHeat > heatCapacity && firedArcs.values().stream().anyMatch(l -> l.contains(true))) { return Messages.getString("WeaponAttackAction.HeatOverCap"); } } From 76b44413143d8c4e45fad689403b63e6f1f33a31 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Tue, 20 Aug 2024 17:18:03 -0500 Subject: [PATCH 08/20] refactor: Use stream composition for adding up non-bay DropShip heat --- .../common/actions/WeaponAttackAction.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 986638485c2..d72e8f78a1c 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1461,21 +1461,15 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } else if (ae instanceof Dropship) { - int totalheat = 0; - - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - Object o = i.nextElement(); - if (!(o instanceof WeaponAttackAction)) { - continue; - } - WeaponAttackAction prevAttack = (WeaponAttackAction) o; - if ((prevAttack.getEntityId() == attackerId) && (weaponId != prevAttack.getWeaponId())) { - Mounted prevWeapon = ae.getEquipment(prevAttack.getWeaponId()); - totalheat += prevWeapon.getCurrentHeat(); - } - } + int totalHeat = game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .map(WeaponAttackAction::getWeaponId) + .filter(id -> id != weaponId) + .mapToInt(id -> ((WeaponMounted) ae.getEquipment(id)).getCurrentHeat()) + .sum(); - if (weapon != null && ((totalheat + weapon.getCurrentHeat()) > heatCapacity)) { + if (Optional.ofNullable(weapon).map(currentWeapon -> totalHeat + currentWeapon.getCurrentHeat() > heatCapacity).orElse(false)) { return Messages.getString("WeaponAttackAction.HeatOverCap"); } } From ca0bbd2f3f405a6ee30732e6b19c84dff36efb22 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Tue, 20 Aug 2024 18:01:34 -0500 Subject: [PATCH 09/20] refactor: Use stream composition for strike attack targeting --- .../common/actions/WeaponAttackAction.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index d72e8f78a1c..75579a357a0 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1681,19 +1681,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // can only make a strike attack against a single target - if (!isStrafing) { - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - EntityAction ea = i.nextElement(); - if (!(ea instanceof WeaponAttackAction)) { - continue; - } - - WeaponAttackAction prevAttk = (WeaponAttackAction) ea; - if ((prevAttk.getEntityId() == ae.getId()) && (prevAttk.getTargetId() != target.getId()) - && !wtype.hasFlag(WeaponType.F_ALT_BOMB)) { - return Messages.getString("WeaponAttackAction.CantSplitFire"); - } - } + if (!isStrafing + && !wtype.hasFlag(WeaponType.F_ALT_BOMB) + && game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .anyMatch(prevAttk -> prevAttk.getEntityId() == ae.getId() && prevAttk.getTargetId() != target.getId())) { + return Messages.getString("WeaponAttackAction.CantSplitFire"); } // VTOL Strafing } else if ((ae instanceof VTOL) && isStrafing) { From c2827f9671ce7cd45ebd84c2497994bd5dadc42e Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Tue, 20 Aug 2024 19:06:45 -0500 Subject: [PATCH 10/20] refactor: Use stream composition for firing Arrow IV bays --- .../megamek/common/actions/WeaponAttackAction.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 75579a357a0..2234ad950ac 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1733,13 +1733,14 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // and finally, can only use Arrow IV artillery if (ae.usesWeaponBays()) { //For Dropships - for (WeaponMounted bayW : weapon.getBayWeapons()) { + if (weapon.streamBayWeapons() // check the loaded ammo for the Arrow IV flag - AmmoMounted bayWAmmo = bayW.getLinkedAmmo(); - AmmoType bAType = bayWAmmo.getType(); - if (bAType.getAmmoType() != AmmoType.T_ARROW_IV) { - return Messages.getString("WeaponAttackAction.OnlyArrowArty"); - } + .map(WeaponMounted::getLinkedAmmo) + .map(AmmoMounted::getType) + .map(AmmoType::getAmmoType) + .anyMatch(bAType -> bAType != AmmoType.T_ARROW_IV) + ) { + return Messages.getString("WeaponAttackAction.OnlyArrowArty"); } } else if ((wtype.getAmmoType() != AmmoType.T_ARROW_IV) && (wtype.getAmmoType() != AmmoType.T_ARROW_IV_BOMB)) { From d061789889eeb5bf6b74fb8ca26b32e7fd104d35 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Tue, 20 Aug 2024 19:43:19 -0500 Subject: [PATCH 11/20] refactor: Use stream composition for BA AP attacks --- .../common/actions/WeaponAttackAction.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 2234ad950ac..a8573b47214 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1866,20 +1866,15 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if ((ae instanceof BattleArmor) && wtype.hasFlag(WeaponType.F_INFANTRY)) { final int weapId = ae.getEquipmentNum(weapon); // See if this unit has made a previous AP attack - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - Object o = i.nextElement(); - if (!(o instanceof WeaponAttackAction)) { - continue; - } - WeaponAttackAction prevAttack = (WeaponAttackAction) o; + if (game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) // Is this an attack from this entity - if (prevAttack.getEntityId() == ae.getId()) { - Mounted prevWeapon = ae.getEquipment(prevAttack.getWeaponId()); - WeaponType prevWtype = (WeaponType) prevWeapon.getType(); - if (prevWtype.hasFlag(WeaponType.F_INFANTRY) && (prevAttack.getWeaponId() != weapId)) { - return Messages.getString("WeaponAttackAction.OnlyOneBAAPAttack"); - } - } + .filter(prevAttack -> prevAttack.getEntityId() == ae.getId()) + .map(WeaponAttackAction::getWeaponId) + .anyMatch(prevAttackId -> prevAttackId != weapId && ae.getEquipment(prevAttackId).getType().hasFlag(WeaponType.F_INFANTRY)) + ) { + return Messages.getString("WeaponAttackAction.OnlyOneBAAPAttack"); } } From 564eb35ed6ac3dbdf414ce8ecc4ae08074f9cf63 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Wed, 21 Aug 2024 14:24:59 -0500 Subject: [PATCH 12/20] refactor: Use stream composition for BA Narc targeting --- .../common/actions/WeaponAttackAction.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index a8573b47214..019c70c3f11 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1881,20 +1881,16 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // BA compact narc: we have one weapon for each trooper, but you // can fire only at one target at a time if (wtype.getName().equals("Compact Narc")) { - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - EntityAction ea = i.nextElement(); - if (!(ea instanceof WeaponAttackAction)) { - continue; - } - final WeaponAttackAction prevAttack = (WeaponAttackAction) ea; - if (prevAttack.getEntityId() == attackerId) { - Mounted prevWeapon = ae.getEquipment(prevAttack.getWeaponId()); - if (prevWeapon.getType().getName().equals("Compact Narc")) { - if (prevAttack.getTargetId() != target.getId()) { - return Messages.getString("WeaponAttackAction.OneTargetForCNarc"); - } - } - } + if (game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .anyMatch(prevAttack -> prevAttack.getEntityId() == attackerId + && ae.getEquipment(prevAttack.getWeaponId()) + .getType().getName().equals("Compact Narc") + && prevAttack.getTargetId() != target.getId() + ) + ) { + return Messages.getString("WeaponAttackAction.OneTargetForCNarc"); } } From c59a7535bd55d4133060685ad44845fed0e89dba Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Thu, 22 Aug 2024 08:48:04 -0500 Subject: [PATCH 13/20] refactor: Use stream composition for firing BA tasers/Narcs This section of code specifically checks whether a BA squad has already fired a taser or Narc at a different target. --- .../common/actions/WeaponAttackAction.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 019c70c3f11..e07a1375b4c 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -1906,23 +1906,25 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && (wtype.hasFlag(WeaponType.F_TASER) || wtype.getAmmoType() == AmmoType.T_NARC)) { // Go through all of the current actions to see if a NARC or Taser // has been fired - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - Object o = i.nextElement(); - if (!(o instanceof WeaponAttackAction)) { - continue; + + // A lazy stream that evaluates to the weapon types this entity has already fired at other targets. + Stream diffTargetWeaponTypes = game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .filter(prevAttack -> + // Is this an attack from this entity to a different target? + prevAttack.getEntityId() == ae.getId() && prevAttack.getTargetId() != target.getId() + ) + .map(prevAttack -> (WeaponType) ae.getEquipment(prevAttack.getWeaponId()).getType()); + + if (wtype.hasFlag(WeaponType.F_TASER)) { + if (diffTargetWeaponTypes.anyMatch(prevWtype -> prevWtype.hasFlag(WeaponType.F_TASER))) { + return Messages.getString("WeaponAttackAction.BATaserSameTarget"); } - WeaponAttackAction prevAttack = (WeaponAttackAction) o; - // Is this an attack from this entity to a different target? - if (prevAttack.getEntityId() == ae.getId() && prevAttack.getTargetId() != target.getId()) { - Mounted prevWeapon = ae.getEquipment(prevAttack.getWeaponId()); - WeaponType prevWtype = (WeaponType) prevWeapon.getType(); - if (prevWeapon.getType().hasFlag(WeaponType.F_TASER) - && weapon.getType().hasFlag(WeaponType.F_TASER)) { - return Messages.getString("WeaponAttackAction.BATaserSameTarget"); - } - if (prevWtype.getAmmoType() == AmmoType.T_NARC && wtype.getAmmoType() == AmmoType.T_NARC) { - return Messages.getString("WeaponAttackAction.BANarcSameTarget"); - } + } else { + assert wtype.getAmmoType() == AmmoType.T_NARC; + if (diffTargetWeaponTypes.anyMatch(prevWtype -> prevWtype.getAmmoType() == AmmoType.T_NARC)) { + return Messages.getString("WeaponAttackAction.BANarcSameTarget"); } } } From 720a88ee08ce7bb61300ef49255d2fd5f342791d Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 07:33:17 -0500 Subject: [PATCH 14/20] refactor: Use stream composition for firing field guns It may be possible to optimize this further if it can be assumed that all CI attacks either have the infantry flag or are field guns, but I've settled for replicating the previous logic for now. --- .../common/actions/WeaponAttackAction.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index e07a1375b4c..fecce80c219 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -2168,21 +2168,23 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.CantMoveAndFieldGun"); } // check for mixing infantry and field gun attacks - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - EntityAction ea = i.nextElement(); - if (!(ea instanceof WeaponAttackAction)) { - continue; - } - final WeaponAttackAction prevAttack = (WeaponAttackAction) ea; - if (prevAttack.getEntityId() == attackerId) { - Mounted prevWeapon = ae.getEquipment(prevAttack.getWeaponId()); - if ((prevWeapon.getType().hasFlag(WeaponType.F_INFANTRY) - && (weapon.getLocation() == Infantry.LOC_FIELD_GUNS)) - || (weapon.getType().hasFlag(WeaponType.F_INFANTRY) - && (prevWeapon.getLocation() == Infantry.LOC_FIELD_GUNS))) { - return Messages.getString("WeaponAttackAction.FieldGunOrSAOnly"); - } - } + if (game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .filter(prevAttack -> prevAttack.getEntityId() == attackerId) + .map(prevAttack -> ae.getEquipment(prevAttack.getWeaponId())) + .anyMatch(prevWeapon -> + ( + prevWeapon.getType().hasFlag(WeaponType.F_INFANTRY) + && weapon.getLocation() == Infantry.LOC_FIELD_GUNS + ) + || ( + prevWeapon.getLocation() == Infantry.LOC_FIELD_GUNS + && weapon.getType().hasFlag(WeaponType.F_INFANTRY) + ) + ) + ) { + return Messages.getString("WeaponAttackAction.FieldGunOrSAOnly"); } } From bdfb73b4da7be81ef48e379900445ffacedad638 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 11:14:04 -0500 Subject: [PATCH 15/20] refactor: Use stream composition when checking for previously-fired solo weapons --- .../common/actions/WeaponAttackAction.java | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index fecce80c219..684c1b8ef8b 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -2408,26 +2408,21 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // Some weapons can only be fired by themselves // Check to see if another solo weapon was fired - boolean hasSoloAttack = false; - String soloWeaponName = ""; - for (EntityAction ea : game.getActionsVector()) { - if ((ea.getEntityId() == attackerId) && (ea instanceof WeaponAttackAction)) { - WeaponAttackAction otherWAA = (WeaponAttackAction) ea; - final Mounted otherWeapon = ae.getEquipment(otherWAA.getWeaponId()); - - if (!(otherWeapon.getType() instanceof WeaponType)) { - continue; - } - final WeaponType otherWtype = (WeaponType) otherWeapon.getType(); - hasSoloAttack |= (otherWtype.hasFlag(WeaponType.F_SOLO_ATTACK) && otherWAA.getWeaponId() != weaponId); - if (hasSoloAttack) { - soloWeaponName = otherWeapon.getName(); - break; - } - } - } - if (hasSoloAttack) { - return String.format(Messages.getString("WeaponAttackAction.CantFireWithOtherWeapons"), soloWeaponName); + + // The name of a solo weapon that has already been fired, if one exists. + Optional soloWeaponName = game.getActionsVector().stream() + .filter(prevAttack -> prevAttack.getEntityId() == attackerId) + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .map(WeaponAttackAction::getWeaponId) + .filter(otherWAAId -> otherWAAId != weaponId) + .map(otherWAAId -> ae.getEquipment(otherWAAId)) + .filter(otherWeapon -> otherWeapon.getType().hasFlag(WeaponType.F_SOLO_ATTACK)) + .map(Mounted::getName) + .findAny(); + + if (soloWeaponName.isPresent()) { + return String.format(Messages.getString("WeaponAttackAction.CantFireWithOtherWeapons"), soloWeaponName.orElseThrow()); } // Handle solo attack weapons. From d16dce551163e3eea1144c2bea25f171d5d6f9c6 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 11:42:13 -0500 Subject: [PATCH 16/20] refactor: Use stream composition when firing solo weapons --- .../common/actions/WeaponAttackAction.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 684c1b8ef8b..7016151439f 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -2426,19 +2426,14 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // Handle solo attack weapons. - if (wtype.hasFlag(WeaponType.F_SOLO_ATTACK)) { - for (EntityAction ea : game.getActionsVector()) { - if (!(ea instanceof WeaponAttackAction)) { - continue; - } - WeaponAttackAction prevAttack = (WeaponAttackAction) ea; - if (prevAttack.getEntityId() == attackerId) { - // If the attacker fires another weapon, this attack fails. - if (weaponId != prevAttack.getWeaponId()) { - return Messages.getString("WeaponAttackAction.CantMixAttacks"); - } - } - } + if (wtype.hasFlag(WeaponType.F_SOLO_ATTACK) && game.getActionsVector().stream() + .filter(prevAttack -> prevAttack.getEntityId() == attackerId) + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + // If the attacker fires another weapon, this attack fails. + .anyMatch(prevAttack -> prevAttack.getEntityId() == attackerId && prevAttack.getWeaponId() != weaponId) + ) { + return Messages.getString("WeaponAttackAction.CantMixAttacks"); } // Protomechs cannot fire arm weapons and main gun in the same turn From 891a17ddefdaa220014a4b9c88de45d14401f8eb Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 13:59:54 -0500 Subject: [PATCH 17/20] refactor: Use stream composition for firing ProtoMech main/arm guns --- .../common/actions/WeaponAttackAction.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 7016151439f..dcf5fe0c577 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -41,6 +41,7 @@ import java.io.Serializable; import java.util.*; import java.util.stream.Stream; +import java.util.stream.IntStream; import java.util.stream.Collectors; /** @@ -2437,21 +2438,28 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // Protomechs cannot fire arm weapons and main gun in the same turn - if ((ae instanceof Protomech) - && ((weapon.getLocation() == Protomech.LOC_MAINGUN) - || (weapon.getLocation() == Protomech.LOC_RARM) - || (weapon.getLocation() == Protomech.LOC_LARM))) { - final boolean firingMainGun = weapon.getLocation() == Protomech.LOC_MAINGUN; - for (EntityAction ea : game.getActionsVector()) { - if ((ea.getEntityId() == attackerId) && (ea instanceof WeaponAttackAction)) { - WeaponAttackAction otherWAA = (WeaponAttackAction) ea; - final Mounted otherWeapon = ae.getEquipment(otherWAA.getWeaponId()); - if ((firingMainGun && ((otherWeapon.getLocation() == Protomech.LOC_RARM) - || (otherWeapon.getLocation() == Protomech.LOC_LARM))) - || !firingMainGun && (otherWeapon.getLocation() == Protomech.LOC_MAINGUN)) { + if (ae instanceof Protomech) { + // A lazy stream that evaluates to the locations of weapons already fired by the attacker. + IntStream firedWeaponLocations = game.getActionsVector().stream() + .filter(ea -> ea.getEntityId() == attackerId) + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .map(otherWAA -> ae.getEquipment(otherWAA.getWeaponId())) + .mapToInt(Mounted::getLocation); + + switch (weapon.getLocation()) { + case Protomech.LOC_MAINGUN: + if (firedWeaponLocations.anyMatch(otherWeaponLocation -> + otherWeaponLocation == Protomech.LOC_LARM || otherWeaponLocation == Protomech.LOC_RARM + )) { + return Messages.getString("WeaponAttackAction.CantFireArmsAndMainGun"); + } + break; + case Protomech.LOC_LARM: + case Protomech.LOC_RARM: + if (firedWeaponLocations.anyMatch(otherWeaponLocation -> otherWeaponLocation == Protomech.LOC_MAINGUN)) { return Messages.getString("WeaponAttackAction.CantFireArmsAndMainGun"); } - } } } From b21fe79a71b0b45b4120435a561cc819b17ac649 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 15:51:23 -0500 Subject: [PATCH 18/20] refactor: Use stream composition for air-to-ground target modifiers --- .../common/actions/WeaponAttackAction.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index dcf5fe0c577..5b94efd80ca 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -3676,18 +3676,16 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ } // units making air to ground attacks are easier to hit by air-to-air // attacks - if (Compute.isAirToAir(ae, target)) { - for (Enumeration i = game.getActions(); i.hasMoreElements();) { - EntityAction ea = i.nextElement(); - if (!(ea instanceof WeaponAttackAction)) { - continue; - } - WeaponAttackAction prevAttack = (WeaponAttackAction) ea; - if ((te != null && prevAttack.getEntityId() == te.getId()) && prevAttack.isAirToGround(game)) { - toHit.addModifier(-3, Messages.getString("WeaponAttackAction.TeGroundAttack")); - break; - } - } + + // The ID of the entity being targeted. + final Optional targetEntityId = Optional.ofNullable(te).map(Entity::getId); + + if (Compute.isAirToAir(ae, target) && targetEntityId.isPresent() && game.getActionsVector().stream() + .filter(WeaponAttackAction.class::isInstance) + .map(WeaponAttackAction.class::cast) + .anyMatch(prevAttack -> prevAttack.getEntityId() == targetEntityId.orElseThrow() && prevAttack.isAirToGround(game)) + ) { + toHit.addModifier(-3, Messages.getString("WeaponAttackAction.TeGroundAttack")); } // grounded aero if (!ae.isAirborne() && !ae.isSpaceborne()) { From 12ed9477aaab7c9d99d8e02d79b1a4694cc3ac96 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 16:22:20 -0500 Subject: [PATCH 19/20] refactor: Use stream composition for sensor shadows --- .../common/actions/WeaponAttackAction.java | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 5b94efd80ca..d71f1a4d9a3 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -4210,7 +4210,9 @@ else if ((atype != null) // Aerospace target modifiers if (te != null && te.isAero() && te.isAirborne()) { - IAero a = (IAero) te; + // Finalized for concurrency reasons + final Entity targetEntity = te; + IAero a = (IAero) targetEntity; // is the target at zero velocity if ((a.getCurrentVelocity() == 0) && !(a.isSpheroid() && !game.getBoard().inSpace())) { @@ -4234,19 +4236,24 @@ else if ((atype != null) // Target hidden in the sensor shadow of a larger spacecraft if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_STRATOPS_SENSOR_SHADOW) && game.getBoard().inSpace()) { - for (Entity en : Compute.getAdjacentEntitiesAlongAttack(ae.getPosition(), target.getPosition(), game)) { - if (!en.isEnemyOf(te) && en.isLargeCraft() - && ((en.getWeight() - te.getWeight()) >= -STRATOPS_SENSOR_SHADOW_WEIGHT_DIFF)) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorShadow")); - break; - } - } - for (Entity en : game.getEntitiesVector(target.getPosition())) { - if (!en.isEnemyOf(te) && en.isLargeCraft() && !en.equals((Entity) a) - && ((en.getWeight() - te.getWeight()) >= -STRATOPS_SENSOR_SHADOW_WEIGHT_DIFF)) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorShadow")); - break; - } + if (Compute.getAdjacentEntitiesAlongAttack(ae.getPosition(), target.getPosition(), game).stream() + .anyMatch(en -> + !en.isEnemyOf(targetEntity) + && en.isLargeCraft() + && en.getWeight() - targetEntity.getWeight() >= -STRATOPS_SENSOR_SHADOW_WEIGHT_DIFF + ) + ) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorShadow")); + } + if (game.getEntitiesVector(target.getPosition()).stream() + .anyMatch(en -> + !en.isEnemyOf(targetEntity) + && en.isLargeCraft() + && !en.equals(targetEntity) + && en.getWeight() - targetEntity.getWeight() >= -STRATOPS_SENSOR_SHADOW_WEIGHT_DIFF + ) + ) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorShadow")); } } } From 014cb8f0b6652fab5c7194c56e62c319c570e8f9 Mon Sep 17 00:00:00 2001 From: Jeremy Saklad Date: Sat, 24 Aug 2024 16:57:27 -0500 Subject: [PATCH 20/20] refactor: Use stream composition for C3 with active probes --- .../common/actions/WeaponAttackAction.java | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index d71f1a4d9a3..450b55e5930 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -4424,26 +4424,14 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.WOODS) || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.JUNGLE)); if (los.canSee() && (targetWoodsAffectModifier || los.thruWoods())) { - boolean bapInRange = Compute.bapInRange(game, ae, te); - boolean c3BAP = false; - if (!bapInRange) { - for (Entity en : game.getC3NetworkMembers(ae)) { - if (ae.equals(en)) { - continue; - } - bapInRange = Compute.bapInRange(game, en, te); - if (bapInRange) { - c3BAP = true; - break; - } - } - } - if (bapInRange) { - if (c3BAP) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BAPInWoodsC3")); - } else { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BAPInWoods")); - } + // Necessary for concurrency reasons + final Entity targetEntity = te; + if (Compute.bapInRange(game, ae, targetEntity)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BAPInWoods")); + } else if (game.getC3NetworkMembers(ae).stream() + .anyMatch(en -> !ae.equals(en) && Compute.bapInRange(game, en, targetEntity)) + ) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BAPInWoodsC3")); } } }