package games.strategy.triplea.delegate;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import games.strategy.engine.data.Change;
import games.strategy.engine.data.CompositeChange;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.RelationshipTracker;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.TerritoryAttachment;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.delegate.IBattle.BattleType;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.player.ITripleAPlayer;
import games.strategy.triplea.ui.MovePanel;
import games.strategy.triplea.util.TransportUtils;
import games.strategy.util.CompositeMatch;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.CompositeMatchOr;
import games.strategy.util.Match;
import games.strategy.util.Util;
public class MovePerformer implements Serializable {
private static final long serialVersionUID = 3752242292777658310L;
private transient AbstractMoveDelegate m_moveDelegate;
private transient IDelegateBridge m_bridge;
private transient PlayerID m_player;
private AAInMoveUtil m_aaInMoveUtil;
private final ExecutionStack m_executionStack = new ExecutionStack();
private UndoableMove m_currentMove;
private Map<Unit, Collection<Unit>> m_newDependents;
private Collection<Unit> arrivingUnits;
MovePerformer() {}
private BattleTracker getBattleTracker() {
return DelegateFinder.battleDelegate(m_bridge.getData()).getBattleTracker();
}
void initialize(final AbstractMoveDelegate delegate) {
m_moveDelegate = delegate;
m_bridge = delegate.getBridge();
m_player = m_bridge.getPlayerID();
if (m_aaInMoveUtil != null) {
m_aaInMoveUtil.initialize(m_bridge);
}
}
private ITripleAPlayer getRemotePlayer(final PlayerID id) {
return (ITripleAPlayer) m_bridge.getRemotePlayer(id);
}
private ITripleAPlayer getRemotePlayer() {
return getRemotePlayer(m_player);
}
void moveUnits(final Collection<Unit> units, final Route route, final PlayerID id,
final Collection<Unit> transportsToLoad, final Map<Unit, Collection<Unit>> newDependents,
final UndoableMove currentMove) {
m_currentMove = currentMove;
m_newDependents = newDependents;
populateStack(units, route, id, transportsToLoad);
m_executionStack.execute(m_bridge);
}
public void resume() {
m_executionStack.execute(m_bridge);
}
/**
* We assume that the move is valid.
*/
private void populateStack(final Collection<Unit> units, final Route route, final PlayerID id,
final Collection<Unit> transportsToLoad) {
final IExecutable preAAFire = new IExecutable() {
private static final long serialVersionUID = -7945930782650355037L;
@Override
public void execute(final ExecutionStack stack, final IDelegateBridge bridge) {
// if we are moving out of a battle zone, mark it
// this can happen for air units moving out of a battle zone
for (final IBattle battle : getBattleTracker().getPendingBattles(route.getStart(), null)) {
for (final Unit unit : units) {
final Route routeUnitUsedToMove = m_moveDelegate.getRouteUsedToMoveInto(unit, route.getStart());
if (battle != null) {
battle.removeAttack(routeUnitUsedToMove, Collections.singleton(unit));
}
}
}
}
};
// hack to allow the executables to share state
final IExecutable fireAA = new IExecutable() {
private static final long serialVersionUID = -3780228078499895244L;
@Override
public void execute(final ExecutionStack stack, final IDelegateBridge bridge) {
final Collection<Unit> aaCasualties = fireAA(route, units);
final Set<Unit> aaCasualtiesWithDependents = new HashSet<>();
// need to remove any dependents here
if (aaCasualties != null) {
aaCasualtiesWithDependents.addAll(aaCasualties);
final Map<Unit, Collection<Unit>> dependencies = TransportTracker.transporting(units, units);
for (final Unit u : aaCasualties) {
final Collection<Unit> dependents = dependencies.get(u);
if (dependents != null) {
aaCasualtiesWithDependents.addAll(dependents);
}
// we might have new dependents too (ie: paratroopers)
final Collection<Unit> newDependents = m_newDependents.get(u);
if (newDependents != null) {
aaCasualtiesWithDependents.addAll(newDependents);
}
}
}
arrivingUnits = Util.difference(units, aaCasualtiesWithDependents);
}
};
final IExecutable postAAFire = new IExecutable() {
private static final long serialVersionUID = 670783657414493643L;
@Override
public void execute(final ExecutionStack stack, final IDelegateBridge bridge) {
// if any non enemy territories on route
// or if any enemy units on route the
// battles on (note water could have enemy but its
// not owned)
final GameData data = bridge.getData();
final CompositeMatch<Territory> mustFightThrough = getMustFightThroughMatch(id, data);
final Collection<Unit> arrived = Collections.unmodifiableList(Util.intersection(units, arrivingUnits));
// Reset Optional
arrivingUnits = new ArrayList<>();
final Collection<Unit> arrivedCopyForBattles = new ArrayList<>(arrived);
final Map<Unit, Unit> transporting = TransportUtils.mapTransports(route, arrived, transportsToLoad);
// If we have paratrooper land units being carried by air units, they should be dropped off in the last
// territory. This means they
// are still dependent during the middle steps of the route.
final Collection<Unit> dependentOnSomethingTilTheEndOfRoute = new ArrayList<>();
final Collection<Unit> airTransports = Match.getMatches(arrived, Matches.UnitIsAirTransport);
final Collection<Unit> paratroops = Match.getMatches(arrived, Matches.UnitIsAirTransportable);
if (!airTransports.isEmpty() && !paratroops.isEmpty()) {
final Map<Unit, Unit> transportingAir =
TransportUtils.mapTransportsToLoad(paratroops, airTransports);
dependentOnSomethingTilTheEndOfRoute.addAll(transportingAir.keySet());
}
final Collection<Unit> presentFromStartTilEnd = new ArrayList<>(arrived);
presentFromStartTilEnd.removeAll(dependentOnSomethingTilTheEndOfRoute);
final CompositeChange change = new CompositeChange();
if (games.strategy.triplea.Properties.getUseFuelCost(data)) {
// markFuelCostResourceChange must be done
// before we load/unload units
change.add(markFuelCostResourceChange(units, route, id, data));
}
markTransportsMovement(arrived, transporting, route);
if (route.someMatch(mustFightThrough) && arrived.size() != 0) {
boolean bombing = false;
boolean ignoreBattle = false;
// could it be a bombing raid
final Collection<Unit> enemyUnits = route.getEnd().getUnits().getMatches(Matches.enemyUnit(id, data));
final Collection<Unit> enemyTargetsTotal = Match.getMatches(enemyUnits,
new CompositeMatchAnd<>(Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(route.getEnd()).invert(),
Matches.unitIsBeingTransported().invert()));
final CompositeMatchOr<Unit> allBombingRaid = new CompositeMatchOr<>(Matches.UnitIsStrategicBomber);
final boolean canCreateAirBattle =
!enemyTargetsTotal.isEmpty()
&& games.strategy.triplea.Properties.getRaidsMayBePreceededByAirBattles(data)
&& AirBattle.territoryCouldPossiblyHaveAirBattleDefenders(route.getEnd(), id, data, true);
if (canCreateAirBattle) {
allBombingRaid.add(Matches.unitCanEscort);
}
final boolean allCanBomb = Match.allMatch(arrived, allBombingRaid);
final Collection<Unit> enemyTargets =
Match.getMatches(enemyTargetsTotal,
Matches.unitIsOfTypes(UnitAttachment
.getAllowedBombingTargetsIntersection(Match.getMatches(arrived, Matches.UnitIsStrategicBomber),
data)));
final boolean targetsOrEscort = !enemyTargets.isEmpty()
|| (!enemyTargetsTotal.isEmpty() && canCreateAirBattle && Match.allMatch(arrived, Matches.unitCanEscort));
boolean targetedAttack = false;
// if it's all bombers and there's something to bomb
if (allCanBomb && targetsOrEscort && GameStepPropertiesHelper.isCombatMove(data)) {
bombing = getRemotePlayer().shouldBomberBomb(route.getEnd());
// if bombing and there's something to target- ask what to bomb
if (bombing) {
// CompositeMatchOr<Unit> unitsToBeBombed = new CompositeMatchOr<Unit>(Matches.UnitIsFactory,
// Matches.UnitCanBeDamagedButIsNotFactory);
// determine which unit to bomb
Unit target;
if (enemyTargets.size() > 1
&& games.strategy.triplea.Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(data)
&& !canCreateAirBattle) {
target = getRemotePlayer().whatShouldBomberBomb(route.getEnd(), enemyTargets, arrived);
} else if (!enemyTargets.isEmpty()) {
target = enemyTargets.iterator().next();
} else {
// in case we are escorts only
target = enemyTargetsTotal.iterator().next();
}
if (target == null) {
bombing = false;
targetedAttack = false;
} else {
targetedAttack = true;
final HashMap<Unit, HashSet<Unit>> targets = new HashMap<>();
targets.put(target, new HashSet<>(arrived));
// createdBattle = true;
getBattleTracker().addBattle(route, arrivedCopyForBattles, bombing, id, m_bridge, m_currentMove,
dependentOnSomethingTilTheEndOfRoute, targets, false);
}
}
}
// Ignore Trn on Trn forces.
if (isIgnoreTransportInMovement(bridge.getData())) {
final boolean allOwnedTransports = Match.allMatch(arrived, Matches.UnitIsTransportButNotCombatTransport);
final boolean allEnemyTransports = Match.allMatch(enemyUnits, Matches.UnitIsTransportButNotCombatTransport);
// If everybody is a transport, don't create a battle
if (allOwnedTransports && allEnemyTransports) {
ignoreBattle = true;
}
}
if (!ignoreBattle && GameStepPropertiesHelper.isCombatMove(data) && !targetedAttack) {
// createdBattle = true;
if (bombing) {
getBattleTracker().addBombingBattle(route, arrivedCopyForBattles, id, m_bridge, m_currentMove,
dependentOnSomethingTilTheEndOfRoute);
} else {
getBattleTracker().addBattle(route, arrivedCopyForBattles, id, m_bridge, m_currentMove,
dependentOnSomethingTilTheEndOfRoute);
}
}
if (!ignoreBattle && GameStepPropertiesHelper.isNonCombatMove(data, false) && !targetedAttack) {
// We are in non-combat move phase, and we are taking over friendly territories. No need for a battle. (This
// could get really
// difficult if we want these recorded in battle records).
for (final Territory t : route
.getMatches(new CompositeMatchAnd<>(
Matches
.territoryIsOwnedByPlayerWhosRelationshipTypeCanTakeOverOwnedTerritoryAndPassableAndNotWater(
id),
Matches.TerritoryIsBlitzable(id, data)))) {
if (Matches.isTerritoryEnemy(id, data).match(t) || Matches.territoryHasEnemyUnits(id, data).match(t)) {
continue;
}
if ((t.equals(route.getEnd()) && Match.allMatch(arrivedCopyForBattles, Matches.UnitIsAir))
|| (!t.equals(route.getEnd()) && Match.allMatch(presentFromStartTilEnd, Matches.UnitIsAir))) {
continue;
}
// createdBattle = true;
getBattleTracker().takeOver(t, id, bridge, m_currentMove, arrivedCopyForBattles);
}
}
}
// mark movement
final Change moveChange = markMovementChange(arrived, route, id);
change.add(moveChange);
// actually move the units
if (route.getStart() != null && route.getEnd() != null) {
// ChangeFactory.addUnits(route.getEnd(), arrived);
Change remove = ChangeFactory.removeUnits(route.getStart(), units);
Change add = ChangeFactory.addUnits(route.getEnd(), arrived);
change.add(add, remove);
}
m_bridge.addChange(change);
m_currentMove.addChange(change);
m_currentMove.setDescription(MyFormatter.unitsToTextNoOwner(arrived) + " moved from "
+ route.getStart().getName() + " to " + route.getEnd().getName());
m_moveDelegate.updateUndoableMoves(m_currentMove);
}
};
m_executionStack.push(postAAFire);
m_executionStack.push(fireAA);
m_executionStack.push(preAAFire);
m_executionStack.execute(m_bridge);
}
private static CompositeMatch<Territory> getMustFightThroughMatch(final PlayerID id, final GameData data) {
final CompositeMatch<Territory> mustFightThrough = new CompositeMatchOr<>();
mustFightThrough.add(Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(id, data));
mustFightThrough.add(Matches.territoryHasNonSubmergedEnemyUnits(id, data));
mustFightThrough
.add(Matches.territoryIsOwnedByPlayerWhosRelationshipTypeCanTakeOverOwnedTerritoryAndPassableAndNotWater(id));
return mustFightThrough;
}
private Change markFuelCostResourceChange(final Collection<Unit> units, final Route route, final PlayerID id,
final GameData data) {
return ChangeFactory.removeResourceCollection(id,
Route.getMovementFuelCostCharge(units, route, id, data /* , mustFight */));
}
private Change markMovementChange(final Collection<Unit> units, final Route route, final PlayerID id) {
final GameData data = m_bridge.getData();
final CompositeChange change = new CompositeChange();
final Territory routeStart = route.getStart();
final TerritoryAttachment taRouteStart = TerritoryAttachment.get(routeStart);
final Territory routeEnd = route.getEnd();
TerritoryAttachment taRouteEnd = null;
if (routeEnd != null) {
taRouteEnd = TerritoryAttachment.get(routeEnd);
}
// only units owned by us need to be marked
final Iterator<Unit> iter = Match.getMatches(units, Matches.unitIsOwnedBy(id)).iterator();
final RelationshipTracker relationshipTracker = data.getRelationshipTracker();
while (iter.hasNext()) {
final TripleAUnit unit = (TripleAUnit) iter.next();
int moved = route.getMovementCost(unit);
final UnitAttachment ua = UnitAttachment.get(unit.getType());
if (ua.getIsAir()) {
if (taRouteStart != null && taRouteStart.getAirBase()
&& relationshipTracker.isAllied(route.getStart().getOwner(), unit.getOwner())) {
moved--;
}
if (taRouteEnd != null && taRouteEnd.getAirBase()
&& relationshipTracker.isAllied(route.getEnd().getOwner(), unit.getOwner())) {
moved--;
}
}
change.add(ChangeFactory.unitPropertyChange(unit, moved + unit.getAlreadyMoved(), TripleAUnit.ALREADY_MOVED));
}
// if neutrals were taken over mark land units with 0 movement
// if entered a non blitzed conquered territory, mark with 0 movement
if (GameStepPropertiesHelper.isCombatMove(data)
&& (MoveDelegate.getEmptyNeutral(route).size() != 0 || hasConqueredNonBlitzed(route))) {
for (final Unit unit : Match.getMatches(units, Matches.UnitIsLand)) {
change.add(ChangeFactory.markNoMovementChange(Collections.singleton(unit)));
}
}
if (routeEnd != null && games.strategy.triplea.Properties.getSubsCanEndNonCombatMoveWithEnemies(data)
&& GameStepPropertiesHelper.isNonCombatMove(data, false) && routeEnd.getUnits()
.someMatch(new CompositeMatchAnd<>(Matches.unitIsEnemyOf(data, id), Matches.UnitIsDestroyer))) {
// if we are allowed to have our subs enter any sea zone with enemies during noncombat, we want to make sure we
// can't keep moving them
// if there is an enemy destroyer there
for (final Unit unit : Match.getMatches(units,
new CompositeMatchAnd<>(Matches.UnitIsSub, Matches.UnitIsAir.invert()))) {
change.add(ChangeFactory.markNoMovementChange(Collections.singleton(unit)));
}
}
return change;
}
/**
* Marks transports and units involved in unloading with no movement left.
*/
private void markTransportsMovement(final Collection<Unit> arrived, final Map<Unit, Unit> transporting,
final Route route) {
if (transporting == null) {
return;
}
final GameData data = m_bridge.getData();
final CompositeMatch<Unit> paratroopNAirTransports = new CompositeMatchOr<>();
paratroopNAirTransports.add(Matches.UnitIsAirTransport);
paratroopNAirTransports.add(Matches.UnitIsAirTransportable);
final boolean paratroopsLanding = Match.someMatch(arrived, paratroopNAirTransports)
&& MoveValidator.allLandUnitsAreBeingParatroopered(arrived);
final Map<Unit, Collection<Unit>> dependentAirTransportableUnits =
MoveValidator.getDependents(Match.getMatches(arrived, Matches.UnitCanTransport));
// add newly created dependents
if (m_newDependents != null) {
for (final Entry<Unit, Collection<Unit>> entry : m_newDependents.entrySet()) {
Collection<Unit> dependents = dependentAirTransportableUnits.get(entry.getKey());
if (dependents != null) {
dependents.addAll(entry.getValue());
} else {
dependents = entry.getValue();
}
dependentAirTransportableUnits.put(entry.getKey(), dependents);
}
}
// If paratroops moved normally (within their normal movement) remove their dependency to the airTransports
// So they can all continue to move normally
if (!paratroopsLanding && !dependentAirTransportableUnits.isEmpty()) {
final Collection<Unit> airTransports = Match.getMatches(arrived, Matches.UnitIsAirTransport);
airTransports.addAll(dependentAirTransportableUnits.keySet());
MovePanel.clearDependents(airTransports);
}
// load the transports
if (route.isLoad() || paratroopsLanding) {
// mark transports as having transported
for (Unit load : transporting.keySet()) {
final Unit transport = transporting.get(load);
if (!TransportTracker.transporting(transport).contains(load)) {
final Change change = TransportTracker.loadTransportChange((TripleAUnit) transport, load);
m_currentMove.addChange(change);
m_currentMove.load(transport);
m_bridge.addChange(change);
}
}
if (transporting.isEmpty()) {
for (final Unit airTransport : dependentAirTransportableUnits.keySet()) {
for (final Unit unit : dependentAirTransportableUnits.get(airTransport)) {
final Change change = TransportTracker.loadTransportChange((TripleAUnit) airTransport, unit);
m_currentMove.addChange(change);
m_currentMove.load(airTransport);
m_bridge.addChange(change);
}
}
}
}
if (route.isUnload() || paratroopsLanding) {
final Set<Unit> units = new HashSet<>();
units.addAll(transporting.values());
units.addAll(transporting.keySet());
// if there are multiple units on a single transport, the transport will be in units list multiple times
if (transporting.isEmpty()) {
units.addAll(dependentAirTransportableUnits.keySet());
for (final Unit airTransport : dependentAirTransportableUnits.keySet()) {
units.addAll(dependentAirTransportableUnits.get(airTransport));
}
}
// any pending battles in the unloading zone?
final BattleTracker tracker = getBattleTracker();
final boolean pendingBattles = tracker.getPendingBattle(route.getStart(), false, BattleType.NORMAL) != null;
for (Unit unit : units) {
if (Matches.UnitIsAir.match(unit)) {
continue;
}
final Unit transportedBy = ((TripleAUnit) unit).getTransportedBy();
// we will unload our paratroopers after they land in battle (after aa guns fire)
if (paratroopsLanding && transportedBy != null && Matches.UnitIsAirTransport.match(transportedBy)
&& GameStepPropertiesHelper.isCombatMove(data)
&& Matches.territoryHasNonSubmergedEnemyUnits(m_player, data).match(route.getEnd())) {
continue;
}
// unload the transports
final Change change1 = TransportTracker.unloadTransportChange((TripleAUnit) unit,
m_currentMove.getRoute().getEnd(), m_player, pendingBattles);
m_currentMove.addChange(change1);
m_currentMove.unload(unit);
m_bridge.addChange(change1);
// set noMovement
final Change change2 = ChangeFactory.markNoMovementChange(Collections.singleton(unit));
m_currentMove.addChange(change2);
m_bridge.addChange(change2);
}
}
}
private boolean hasConqueredNonBlitzed(final Route route) {
final BattleTracker tracker = getBattleTracker();
for (final Territory current : route.getSteps()) {
if (tracker.wasConquered(current) && !tracker.wasBlitzed(current)) {
return true;
}
}
return false;
}
/**
* Fire aa guns. Returns units to remove.
*/
private Collection<Unit> fireAA(final Route route, final Collection<Unit> units) {
if (m_aaInMoveUtil == null) {
m_aaInMoveUtil = new AAInMoveUtil();
}
m_aaInMoveUtil.initialize(m_bridge);
final Collection<Unit> rVal =
m_aaInMoveUtil.fireAA(route, units, UnitComparator.getLowestToHighestMovementComparator(), m_currentMove);
m_aaInMoveUtil = null;
return rVal;
}
private static boolean isIgnoreTransportInMovement(final GameData data) {
return games.strategy.triplea.Properties.getIgnoreTransportInMovement(data);
}
}