Skip to content

Commit 474b788

Browse files
authored
Speed up AI turns and reduce memory and save game sizes. (#10442)
* Speed up AI turns and reduce memory and save game sizes. Speed up AI turns and reduce memory and save game sizes, by not instantiating empty collections for many different properties of attachments. Standardize handling with helpers for getters, which return unmodifiable views of the underlying collections. Introduce an unmodifiable empty IntegerMap and the ability to return an unmodifiable view of an IntegerMap, for consistency with other collection methods. Annotate null fields with `@Nullable` which lets IDEs help identify warnings when used without null checks directly.s.
1 parent 840a430 commit 474b788

25 files changed

+865
-1036
lines changed

game-app/game-core/src/main/java/games/strategy/engine/data/DefaultAttachment.java

+78-18
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@
44

55
import com.google.common.base.Splitter;
66
import com.google.common.collect.Iterables;
7+
import games.strategy.engine.data.gameparser.GameParseException;
78
import games.strategy.triplea.Constants;
89
import java.math.BigDecimal;
10+
import java.util.ArrayList;
11+
import java.util.Collections;
12+
import java.util.HashSet;
13+
import java.util.List;
14+
import java.util.Map;
915
import java.util.Objects;
1016
import java.util.Optional;
17+
import java.util.Set;
18+
import javax.annotation.Nullable;
19+
import lombok.Getter;
20+
import lombok.Setter;
21+
import org.triplea.java.collections.IntegerMap;
1122

1223
/**
1324
* Contains some utility methods that subclasses can use to make writing attachments easier. FYI:
@@ -23,8 +34,8 @@ public abstract class DefaultAttachment extends GameDataComponent implements IAt
2334
private static final long serialVersionUID = -1985116207387301730L;
2435
private static final Splitter COLON_SPLITTER = Splitter.on(':');
2536

26-
private Attachable attachedTo;
27-
private String name;
37+
@Getter @Setter private Attachable attachedTo;
38+
@Getter private String name;
2839

2940
protected DefaultAttachment(
3041
final String name, final Attachable attachable, final GameData gameData) {
@@ -103,29 +114,13 @@ public String getRawPropertyString(final String property) {
103114
return getProperty(property).map(MutableProperty::getValue).map(Object::toString).orElse(null);
104115
}
105116

106-
@Override
107-
public Attachable getAttachedTo() {
108-
return attachedTo;
109-
}
110-
111-
@Override
112-
public void setAttachedTo(final Attachable attachable) {
113-
attachedTo = attachable;
114-
}
115-
116-
@Override
117-
public String getName() {
118-
return name;
119-
}
120-
121117
@Override
122118
public void setName(final String name) {
123119
this.name =
124120
Optional.ofNullable(name)
125121
// replace-all to automatically correct legacy (1.8) attachment spelling
126122
.map(attachmentName -> attachmentName.replaceAll("ttatch", "ttach"))
127123
.orElse(null);
128-
;
129124
}
130125

131126
/**
@@ -156,4 +151,69 @@ public final boolean equals(final Object obj) {
156151
Objects.toString(attachedTo, null), Objects.toString(other.attachedTo, null))
157152
&& (Objects.equals(name, other.name) || this.toString().equals(other.toString()));
158153
}
154+
155+
protected Territory getTerritoryOrThrow(String name) throws GameParseException {
156+
return Optional.ofNullable(getData().getMap().getTerritory(name))
157+
.orElseThrow(() -> new GameParseException("No territory named: " + name + thisErrorMsg()));
158+
}
159+
160+
protected List<GamePlayer> parsePlayerList(final String value, List<GamePlayer> existingList)
161+
throws GameParseException {
162+
for (final String name : splitOnColon(value)) {
163+
if (existingList == null) {
164+
existingList = new ArrayList<>();
165+
}
166+
existingList.add(getPlayerOrThrow(name));
167+
}
168+
return existingList;
169+
}
170+
171+
protected GamePlayer getPlayerOrThrow(String name) throws GameParseException {
172+
return Optional.ofNullable(getData().getPlayerList().getPlayerId(name))
173+
.orElseThrow(() -> new GameParseException("No player named: " + name + thisErrorMsg()));
174+
}
175+
176+
protected Set<UnitType> parseUnitTypes(String context, String value, Set<UnitType> existingSet)
177+
throws GameParseException {
178+
for (final String u : splitOnColon(value)) {
179+
if (existingSet == null) {
180+
existingSet = new HashSet<>();
181+
}
182+
existingSet.add(getUnitTypeOrThrow(u));
183+
}
184+
return existingSet;
185+
}
186+
187+
public UnitType getUnitTypeOrThrow(String unitType) throws GameParseException {
188+
return Optional.ofNullable(getData().getUnitTypeList().getUnitType(unitType))
189+
.orElseThrow(() -> new GameParseException("No unit type: " + unitType + thisErrorMsg()));
190+
}
191+
192+
protected static <T> List<T> getListProperty(@Nullable List<T> value) {
193+
if (value == null) {
194+
return List.of();
195+
}
196+
return Collections.unmodifiableList(value);
197+
}
198+
199+
protected static <K, V> Map<K, V> getMapProperty(@Nullable Map<K, V> value) {
200+
if (value == null) {
201+
return Map.of();
202+
}
203+
return Collections.unmodifiableMap(value);
204+
}
205+
206+
protected static <T> Set<T> getSetProperty(@Nullable Set<T> value) {
207+
if (value == null) {
208+
return Set.of();
209+
}
210+
return Collections.unmodifiableSet(value);
211+
}
212+
213+
protected static <T> IntegerMap<T> getIntegerMapProperty(@Nullable IntegerMap<T> value) {
214+
if (value == null) {
215+
return IntegerMap.of();
216+
}
217+
return IntegerMap.unmodifiableViewOf(value);
218+
}
159219
}

game-app/game-core/src/main/java/games/strategy/triplea/attachments/AbstractConditionsAttachment.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
import java.util.Objects;
2525
import java.util.Optional;
2626
import java.util.Set;
27+
import javax.annotation.Nullable;
2728

2829
/**
2930
* This class is designed to hold common code for holding "conditions". Any attachment that can hold
30-
* conditions (ie: RulesAttachments), should extend this instead of DefaultAttachment.
31+
* conditions (ie: RulesAttachments), should extend this instead of DefaultAttachment. Note: Empty
32+
* collection fields default to null to minimize memory use and serialization size.
3133
*/
3234
public abstract class AbstractConditionsAttachment extends DefaultAttachment implements ICondition {
3335
public static final String TRIGGER_CHANCE_SUCCESSFUL = "Trigger Rolling is a Success!";
@@ -40,7 +42,7 @@ public abstract class AbstractConditionsAttachment extends DefaultAttachment imp
4042
private static final Splitter HYPHEN_SPLITTER = Splitter.on('-');
4143

4244
// list of conditions that this condition can contain
43-
protected List<RulesAttachment> conditions = new ArrayList<>();
45+
protected @Nullable List<RulesAttachment> conditions = null;
4446
// conditionType modifies the relationship of conditions
4547
protected String conditionType = AND;
4648
// will logically negate the entire condition, including contained conditions
@@ -90,11 +92,11 @@ private void setConditions(final List<RulesAttachment> value) {
9092

9193
@Override
9294
public List<RulesAttachment> getConditions() {
93-
return conditions;
95+
return getListProperty(conditions);
9496
}
9597

9698
protected void resetConditions() {
97-
conditions = new ArrayList<>();
99+
conditions = null;
98100
}
99101

100102
private void setInvert(final boolean s) {

game-app/game-core/src/main/java/games/strategy/triplea/attachments/AbstractPlayerRulesAttachment.java

+13-19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import games.strategy.triplea.Constants;
1212
import java.util.Collection;
1313
import java.util.Map;
14+
import javax.annotation.Nullable;
1415
import lombok.extern.slf4j.Slf4j;
1516
import org.triplea.java.collections.IntegerMap;
1617

@@ -24,7 +25,8 @@
2425
* ownership of territories, or objectiveValue (the money given if the condition is true), would NOT
2526
* go in This class. <br>
2627
* Please do not add new things to this class. Any new Player-Rules type of stuff should go in
27-
* "PlayerAttachment".
28+
* "PlayerAttachment". Note: Empty collection fields default to null to minimize memory use and
29+
* serialization size.
2830
*/
2931
@Slf4j
3032
public abstract class AbstractPlayerRulesAttachment extends AbstractRulesAttachment {
@@ -34,8 +36,8 @@ public abstract class AbstractPlayerRulesAttachment extends AbstractRulesAttachm
3436
// These variables are related to a "rulesAttachment" that changes certain rules for the attached
3537
// player. They are
3638
// not related to conditions at all.
37-
protected String movementRestrictionType = null;
38-
protected String[] movementRestrictionTerritories = null;
39+
protected @Nullable String movementRestrictionType = null;
40+
protected @Nullable String[] movementRestrictionTerritories = null;
3941
// allows placing units in any owned land
4042
protected boolean placementAnyTerritory = false;
4143
// allows placing units in any sea by owned land
@@ -51,7 +53,7 @@ public abstract class AbstractPlayerRulesAttachment extends AbstractRulesAttachm
5153
// negates dominatingFirstRoundAttack
5254
protected boolean negateDominatingFirstRoundAttack = false;
5355
// automatically produces 1 unit of a certain
54-
protected IntegerMap<UnitType> productionPerXTerritories = new IntegerMap<>();
56+
protected @Nullable IntegerMap<UnitType> productionPerXTerritories = null;
5557
// type per every X territories owned
5658
// stops the user from placing units in any territory that already contains more than this
5759
protected int placementPerTerritory = -1;
@@ -117,10 +119,6 @@ public static ICondition getCondition(
117119
}
118120

119121
private void setMovementRestrictionTerritories(final String value) {
120-
if (value == null) {
121-
movementRestrictionTerritories = null;
122-
return;
123-
}
124122
movementRestrictionTerritories = splitOnColon(value);
125123
validateNames(movementRestrictionTerritories);
126124
}
@@ -138,18 +136,14 @@ private void resetMovementRestrictionTerritories() {
138136
}
139137

140138
private void setMovementRestrictionType(final String value) throws GameParseException {
141-
if (value == null) {
142-
movementRestrictionType = null;
143-
return;
144-
}
145139
if (!(value.equals("disallowed") || value.equals("allowed"))) {
146140
throw new GameParseException(
147141
"movementRestrictionType must be allowed or disallowed" + thisErrorMsg());
148142
}
149143
movementRestrictionType = value;
150144
}
151145

152-
public String getMovementRestrictionType() {
146+
public @Nullable String getMovementRestrictionType() {
153147
return movementRestrictionType;
154148
}
155149

@@ -171,15 +165,15 @@ private void setProductionPerXTerritories(final String value) throws GameParseEx
171165
unitTypeToProduce = s[1];
172166
}
173167
// validate that this unit exists in the xml
174-
final UnitType ut = getData().getUnitTypeList().getUnitType(unitTypeToProduce);
175-
if (ut == null) {
176-
throw new GameParseException("No unit called: " + unitTypeToProduce + thisErrorMsg());
177-
}
168+
final UnitType ut = getUnitTypeOrThrow(unitTypeToProduce);
178169
final int n = getInt(s[0]);
179170
if (n <= 0) {
180171
throw new GameParseException(
181172
"productionPerXTerritories must be a positive integer" + thisErrorMsg());
182173
}
174+
if (productionPerXTerritories == null) {
175+
productionPerXTerritories = new IntegerMap<>();
176+
}
183177
productionPerXTerritories.put(ut, n);
184178
}
185179

@@ -188,11 +182,11 @@ private void setProductionPerXTerritories(final IntegerMap<UnitType> value) {
188182
}
189183

190184
public IntegerMap<UnitType> getProductionPerXTerritories() {
191-
return productionPerXTerritories;
185+
return getIntegerMapProperty(productionPerXTerritories);
192186
}
193187

194188
private void resetProductionPerXTerritories() {
195-
productionPerXTerritories = new IntegerMap<>();
189+
productionPerXTerritories = null;
196190
}
197191

198192
private void setPlacementPerTerritory(final String value) {

game-app/game-core/src/main/java/games/strategy/triplea/attachments/AbstractRulesAttachment.java

+19-25
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,25 @@
88
import games.strategy.engine.data.GamePlayer;
99
import games.strategy.engine.data.GameState;
1010
import games.strategy.engine.data.MutableProperty;
11-
import games.strategy.engine.data.PlayerList;
1211
import games.strategy.engine.data.Territory;
1312
import games.strategy.engine.data.gameparser.GameParseException;
1413
import games.strategy.triplea.delegate.Matches;
1514
import games.strategy.triplea.delegate.OriginalOwnerTracker;
16-
import java.util.ArrayList;
1715
import java.util.Collection;
16+
import java.util.Collections;
1817
import java.util.HashMap;
1918
import java.util.HashSet;
2019
import java.util.List;
2120
import java.util.Map;
2221
import java.util.Set;
22+
import javax.annotation.Nullable;
2323
import org.triplea.java.RenameOnNextMajorRelease;
2424
import org.triplea.java.collections.CollectionUtils;
2525

26-
/** The Purpose of this class is to hold shared and simple methods used by RulesAttachment. */
26+
/**
27+
* The Purpose of this class is to hold shared and simple methods used by RulesAttachment. Note:
28+
* Empty collection fields default to null to minimize memory use and serialization size.
29+
*/
2730
public abstract class AbstractRulesAttachment extends AbstractConditionsAttachment {
2831
private static final long serialVersionUID = -6977650137928964759L;
2932

@@ -33,17 +36,17 @@ public abstract class AbstractRulesAttachment extends AbstractConditionsAttachme
3336
// directPresenceTerritories, or any of the other territory lists
3437
// only used if the attachment begins with "objectiveAttachment"
3538
// Note: Subclasses should use getPlayers() which take into account getAttachedTo().
36-
private List<GamePlayer> players = new ArrayList<>();
39+
private @Nullable List<GamePlayer> players = null;
3740
protected int objectiveValue = 0;
3841
// only matters for objectiveValue, does not affect the condition
3942
protected int uses = -1;
4043
// condition for what turn it is
4144
@RenameOnNextMajorRelease(newName = "rounds")
42-
protected Map<Integer, Integer> turns = null;
45+
protected @Nullable Map<Integer, Integer> turns = null;
4346
// for on/off conditions
4447
protected boolean switched = true;
4548
// allows custom GameProperties
46-
protected String gameProperty = null;
49+
protected @Nullable String gameProperty = null;
4750
// Determines if we will be counting each for the purposes of objectiveValue
4851
private boolean countEach = false;
4952
// Used with the next Territory conditions to determine the number of territories needed to be
@@ -56,26 +59,21 @@ protected AbstractRulesAttachment(
5659
}
5760

5861
private void setPlayers(final String names) throws GameParseException {
59-
final PlayerList pl = getData().getPlayerList();
60-
for (final String p : splitOnColon(names)) {
61-
final GamePlayer player = pl.getPlayerId(p);
62-
if (player == null) {
63-
throw new GameParseException("Could not find player. name:" + p + thisErrorMsg());
64-
}
65-
players.add(player);
66-
}
62+
players = parsePlayerList(names, players);
6763
}
6864

6965
private void setPlayers(final List<GamePlayer> value) {
7066
players = value;
7167
}
7268

7369
protected List<GamePlayer> getPlayers() {
74-
return players.isEmpty() ? new ArrayList<>(List.of((GamePlayer) getAttachedTo())) : players;
70+
return players == null
71+
? List.of((GamePlayer) getAttachedTo())
72+
: Collections.unmodifiableList(players);
7573
}
7674

7775
private void resetPlayers() {
78-
players = new ArrayList<>();
76+
players = null;
7977
}
8078

8179
@Override
@@ -166,7 +164,7 @@ private void setGameProperty(final String value) {
166164
gameProperty = value;
167165
}
168166

169-
private String getGameProperty() {
167+
private @Nullable String getGameProperty() {
170168
return gameProperty;
171169
}
172170

@@ -179,10 +177,6 @@ private void resetGameProperty() {
179177
}
180178

181179
private void setRounds(final String rounds) throws GameParseException {
182-
if (rounds == null) {
183-
turns = null;
184-
return;
185-
}
186180
turns = new HashMap<>();
187181
final String[] s = splitOnColon(rounds);
188182
if (s.length < 1) {
@@ -217,17 +211,17 @@ private void setRounds(final Map<Integer, Integer> value) {
217211

218212
@VisibleForTesting
219213
public Map<Integer, Integer> getRounds() {
220-
return turns;
214+
return getMapProperty(turns);
221215
}
222216

223217
private void resetRounds() {
224218
turns = null;
225219
}
226220

227-
protected boolean checkTurns(final GameState data) {
221+
protected boolean checkRounds(final GameState data) {
228222
final int turn = data.getSequence().getRound();
229-
for (final int t : turns.keySet()) {
230-
if (turn >= t && turn <= turns.get(t)) {
223+
for (final Map.Entry<Integer, Integer> entry : getRounds().entrySet()) {
224+
if (turn >= entry.getKey() && turn <= entry.getValue()) {
231225
return true;
232226
}
233227
}

0 commit comments

Comments
 (0)