Skip to content

Commit d537df7

Browse files
authored
Merge branch 'main' into checktyle-adjustments
2 parents 3a5e197 + 2a297c9 commit d537df7

16 files changed

+596
-37
lines changed

MekHQ/resources/mekhq/resources/AdvancedReplacementLimbDialog.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ AdvancedReplacementLimbDialog.status.selected=<b>Selected:</b> {0} surgery(s)
4242
AdvancedReplacementLimbDialog.status.difficulty=<b>Surgery Skill Level Required:</b> {0}
4343
AdvancedReplacementLimbDialog.status.localSurgeon=<b>Using Local Surgeon</b> Cost increased tenfold
4444
AdvancedReplacementLimbDialog.status.localSurgeon.transit=<b>Unable to find a local surgeon in space</b>
45-
AdvancedReplacementLimbDialog.status.surgeon=<b>Surgeon:</b> {0} ({1}+)
45+
AdvancedReplacementLimbDialog.status.surgeon=<b>Surgeon:</b> {0} (Skill Level {1}, TN {2}+)
4646
AdvancedReplacementLimbDialog.status.total=<b>Total Cost:</b> {0} C-Bills
4747
AdvancedReplacementLimbDialog.exclusions.refused=<br>- {0}Patient has refused due to hatred of bionics.{1}
4848
AdvancedReplacementLimbDialog.exclusions.planet=<br>- {0}Advanced prosthetics require planetary facilities.{1}

MekHQ/resources/mekhq/resources/CreateCharacterDialog.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ age=years old
6969
textBloodname.error=Generation Failed
7070
optionGroup.lvl3Advantages=Advantages
7171
optionGroup.edgeAdvantages=Edge
72+
optionGroup.eiAdvantages=Enhanced Imaging
7273
optionGroup.MDAdvantages=Warrior Augmentations
7374
txtDesc.title=Instructions
7475
panXpLeft.title=XP remaining

MekHQ/resources/mekhq/resources/CustomizePersonDialog.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ age=years old
7070
textBloodname.error=Generation Failed
7171
optionGroup.lvl3Advantages=Advantages
7272
optionGroup.edgeAdvantages=Edge
73+
optionGroup.eiAdvantages=Enhanced Imaging
7374
optionGroup.MDAdvantages=Warrior Augmentations

MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.
2+
* Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.
33
*
44
* This file is part of MekHQ.
55
*
@@ -88,7 +88,6 @@
8888
import megamek.common.containers.MunitionTree;
8989
import megamek.common.enums.Gender;
9090
import megamek.common.enums.SkillLevel;
91-
import megamek.common.equipment.GunEmplacement;
9291
import megamek.common.equipment.MiscType;
9392
import megamek.common.equipment.Transporter;
9493
import megamek.common.equipment.WeaponMounted;
@@ -1828,9 +1827,12 @@ public static void setTerrain(AtBDynamicScenario scenario) {
18281827
scenario.setBoardType(T_GROUND);
18291828
StratConBiomeManifest biomeManifest = StratConBiomeManifest.getInstance();
18301829
int kelvinTemp = scenario.getTemperature() + StratConContractInitializer.ZERO_CELSIUS_IN_KELVIN;
1831-
List<String> allowedTerrain = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_BIOME)
1832-
.floorEntry(kelvinTemp)
1833-
.getValue().allowedTerrainTypes;
1830+
var tempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_BIOME);
1831+
var biomeEntry = tempMap.floorEntry(kelvinTemp);
1832+
if (biomeEntry == null) {
1833+
biomeEntry = tempMap.firstEntry();
1834+
}
1835+
List<String> allowedTerrain = biomeEntry.getValue().allowedTerrainTypes;
18341836

18351837
int terrainIndex = randomInt(allowedTerrain.size());
18361838
scenario.setTerrainType(allowedTerrain.get(terrainIndex));
@@ -1847,12 +1849,18 @@ public static void setTerrain(AtBDynamicScenario scenario) {
18471849
} else {
18481850
StratConBiomeManifest biomeManifest = StratConBiomeManifest.getInstance();
18491851
int kelvinTemp = scenario.getTemperature() + StratConContractInitializer.ZERO_CELSIUS_IN_KELVIN;
1850-
List<String> allowedFacility = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_FACILITY_BIOME)
1851-
.floorEntry(kelvinTemp)
1852-
.getValue().allowedTerrainTypes;
1853-
List<String> allowedTerrain = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_BIOME)
1854-
.floorEntry(kelvinTemp)
1855-
.getValue().allowedTerrainTypes;
1852+
var facilityTempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_FACILITY_BIOME);
1853+
var facilityBiomeEntry = facilityTempMap.floorEntry(kelvinTemp);
1854+
if (facilityBiomeEntry == null) {
1855+
facilityBiomeEntry = facilityTempMap.firstEntry();
1856+
}
1857+
List<String> allowedFacility = facilityBiomeEntry.getValue().allowedTerrainTypes;
1858+
var terrainTempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_BIOME);
1859+
var terrainBiomeEntry = terrainTempMap.floorEntry(kelvinTemp);
1860+
if (terrainBiomeEntry == null) {
1861+
terrainBiomeEntry = terrainTempMap.firstEntry();
1862+
}
1863+
List<String> allowedTerrain = terrainBiomeEntry.getValue().allowedTerrainTypes;
18561864
List<String> allowedTemplate = scenario.getTemplate().mapParameters.allowedTerrainTypes;
18571865
// try to filter on temp
18581866
allowedTerrain.addAll(allowedFacility);

MekHQ/src/mekhq/campaign/mission/ContractScore.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2025 The MegaMek Team. All Rights Reserved.
2+
* Copyright (C) 2025-2026 The MegaMek Team. All Rights Reserved.
33
*
44
* This file is part of MekHQ.
55
*
@@ -53,8 +53,8 @@ public class ContractScore {
5353
private static final int MARGINAL_DEFEAT = -1;
5454
private static final int DEFEAT = -2;
5555
private static final int DECISIVE_DEFEAT = -3;
56-
private static final int FLEET_IN_BEING = 2;
57-
private static final int REFUSED_ENGAGEMENT = 2;
56+
private static final int FLEET_IN_BEING = -2;
57+
private static final int REFUSED_ENGAGEMENT = -3;
5858

5959
/**
6060
* Calculates the overall contract score based on the outcomes of all completed scenarios.

MekHQ/src/mekhq/campaign/personnel/education/Academy.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,26 @@ public List<Integer> getQualificationStartYears() {
549549
return qualificationStartYears;
550550
}
551551

552+
/**
553+
* Validates that the parallel lists (qualifications, curriculums, qualificationStartYears) have matching sizes.
554+
* Logs a warning if they don't match.
555+
*
556+
* @return true if lists are valid (same size or all null/empty), false if mismatched
557+
*/
558+
public boolean validateListSizes() {
559+
int qualCount = (qualifications == null) ? 0 : qualifications.size();
560+
int currCount = (curriculums == null) ? 0 : curriculums.size();
561+
int yearCount = (qualificationStartYears == null) ? 0 : qualificationStartYears.size();
562+
563+
if (qualCount != currCount || qualCount != yearCount) {
564+
LOGGER.warn("Academy '{}' has mismatched list sizes: qualifications={}, curriculums={}, startYears={}. "
565+
+ "This may cause errors when viewing course tooltips.",
566+
name, qualCount, currCount, yearCount);
567+
return false;
568+
}
569+
return true;
570+
}
571+
552572
/**
553573
* Retrieves the base skill level granted by this academy.
554574
*
@@ -812,6 +832,13 @@ public String getTooltip(Campaign campaign, List<Person> personnel, int courseIn
812832
MekHQ.getMHQOptions().getLocale());
813833

814834
try {
835+
// Bounds check: ensure courseIndex is valid for curriculums list
836+
if (curriculums == null || courseIndex < 0 || courseIndex >= curriculums.size()) {
837+
LOGGER.error("Invalid courseIndex {} for academy '{}' with {} curriculums",
838+
courseIndex, name, curriculums == null ? 0 : curriculums.size());
839+
return "<html><body style='width: 200px'><i>Error: Invalid course data</i></body></html>";
840+
}
841+
815842
StringBuilder tooltip = new StringBuilder().append("<html><body style='width: 200px'>");
816843
tooltip.append("<i>").append(description).append("</i><br><br>");
817844
tooltip.append("<b>").append(resources.getString("curriculum.text")).append("</b><br>");

MekHQ/src/mekhq/campaign/personnel/education/AcademyFactory.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2018-2025 The MegaMek Team. All Rights Reserved.
2+
* Copyright (C) 2018-2026 The MegaMek Team. All Rights Reserved.
33
*
44
* This file is part of MekHQ.
55
*
@@ -140,9 +140,40 @@ public void loadAcademyFromStream(InputStream inputStream, String fileName) {
140140
academy.setId(id);
141141
id++;
142142
academy.setSet(currentSetName);
143+
// Validate that parallel lists have matching sizes, skip invalid academies
144+
if (!academy.validateListSizes()) {
145+
LOGGER.warn("Skipping academy '{}' in set '{}' from file '{}' due to invalid list sizes.",
146+
academy.getName(), currentSetName, fileName);
147+
continue;
148+
}
143149
tempAcademyMap.put(academy.getName(), academy);
144150
}
145-
academyMap.put(currentSetName, tempAcademyMap);
151+
152+
// Merge with existing academies instead of replacing the entire set.
153+
// This allows user academy files to override specific academies while
154+
// preserving base academies that aren't in the user's file.
155+
Map<String, Academy> existingMap = academyMap.get(currentSetName);
156+
if (existingMap != null) {
157+
// Find the max ID from existing academies to avoid ID conflicts
158+
int maxId = existingMap.values().stream()
159+
.mapToInt(Academy::getId)
160+
.max().orElse(-1);
161+
// Reassign IDs: preserve IDs for overrides, assign new IDs for new academies
162+
int newId = maxId + 1;
163+
for (Academy academy : tempAcademyMap.values()) {
164+
Academy existing = existingMap.get(academy.getName());
165+
if (existing != null) {
166+
// Preserve the original ID when overriding an existing academy
167+
academy.setId(existing.getId());
168+
} else {
169+
// Assign a new, non-conflicting ID for new academies
170+
academy.setId(newId++);
171+
}
172+
}
173+
existingMap.putAll(tempAcademyMap);
174+
} else {
175+
academyMap.put(currentSetName, tempAcademyMap);
176+
}
146177
} catch (JAXBException e) {
147178
LOGGER.error("Error loading XML for academies", e);
148179
}

MekHQ/src/mekhq/campaign/stratCon/StratConBiomeManifest.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,47 @@ public String getFacilityImage(String facilityType) {
147147
private static StratConBiomeManifest instance;
148148

149149
/**
150-
* Gets the singleton biome manifest instance
150+
* Gets the singleton biome manifest instance.
151+
* If the manifest file cannot be loaded, returns a default instance with minimal biome data.
151152
*/
152153
public static StratConBiomeManifest getInstance() {
153154
if (instance == null) {
154155
instance = load();
156+
if (instance == null) {
157+
logger.warn("Failed to load biome manifest, using default instance");
158+
instance = createDefaultInstance();
159+
}
155160
}
156161

157162
return instance;
158163
}
159164

165+
/**
166+
* Creates a default biome manifest with minimal data for fallback/testing scenarios. This ensures the system can
167+
* function even when the XML configuration is unavailable.
168+
*/
169+
private static StratConBiomeManifest createDefaultInstance() {
170+
StratConBiomeManifest manifest = new StratConBiomeManifest();
171+
172+
// Create a default biome that covers all temperature ranges
173+
StratConBiome defaultBiome = new StratConBiome();
174+
defaultBiome.biomeCategory = TERRAN_BIOME;
175+
defaultBiome.allowedTemperatureLowerBound = Integer.MIN_VALUE;
176+
defaultBiome.allowedTemperatureUpperBound = Integer.MAX_VALUE;
177+
defaultBiome.allowedTerrainTypes = new ArrayList<>();
178+
defaultBiome.allowedTerrainTypes.add("Grasslands");
179+
180+
TreeMap<Integer, StratConBiome> defaultTempMap = new TreeMap<>();
181+
defaultTempMap.put(Integer.MIN_VALUE, defaultBiome);
182+
183+
manifest.biomeTempMap.put(TERRAN_BIOME, defaultTempMap);
184+
manifest.biomeTempMap.put(TERRAN_FACILITY_BIOME, defaultTempMap);
185+
186+
manifest.biomes.add(defaultBiome);
187+
188+
return manifest;
189+
}
190+
160191
private static StratConBiomeManifest load() {
161192
StratConBiomeManifest resultingManifest;
162193
File inputFile = new File(MHQConstants.STRAT_CON_BIOME_MANIFEST_PATH);

MekHQ/src/mekhq/campaign/stratCon/StratConContractInitializer.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.
2+
* Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.
33
*
44
* This file is part of MekHQ.
55
*
@@ -59,6 +59,8 @@
5959
import mekhq.campaign.mission.enums.AtBMoraleLevel;
6060
import mekhq.campaign.stratCon.StratConContractDefinition.ObjectiveParameters;
6161
import mekhq.campaign.stratCon.StratConContractDefinition.StrategicObjectiveType;
62+
import mekhq.campaign.universe.Planet;
63+
import mekhq.campaign.universe.PlanetarySystem;
6264

6365
/**
6466
* This class handles StratCon state initialization when a contract is signed.
@@ -96,7 +98,8 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai
9698
// when objective is allied/hostile facility, place those facilities
9799

98100
int maximumTrackIndex = max(0, contract.getRequiredCombatTeams() / NUM_LANCES_PER_TRACK);
99-
int planetaryTemperature = campaign.getLocation().getPlanet().getTemperature(campaign.getLocalDate());
101+
// Use the contract's destination planet, not the campaign's current location
102+
int planetaryTemperature = getContractPlanetTemperature(contract, campaign);
100103

101104
CampaignOptions campaignOptions = campaign.getCampaignOptions();
102105
boolean isUseMaplessMode = campaignOptions.isUseStratConMaplessMode();
@@ -310,6 +313,45 @@ public static int getScenarioOdds(StratConContractDefinition contractDefinition)
310313
return contractDefinition.getScenarioOdds().get(Compute.randomInt(contractDefinition.getScenarioOdds().size()));
311314
}
312315

316+
/**
317+
* Gets the temperature of the contract's destination planet.
318+
*
319+
* <p>Uses the contract's system (where the contract takes place), not the campaign's
320+
* current location. Falls back to 25C (standard room temperature) if the planet or temperature data is
321+
* unavailable.</p>
322+
*
323+
* @param contract the contract being initialized
324+
* @param campaign the campaign (used to get the current date)
325+
*
326+
* @return the planetary temperature in Celsius
327+
*/
328+
private static int getContractPlanetTemperature(AtBContract contract, Campaign campaign) {
329+
final int DEFAULT_TEMPERATURE = 25; // Standard room temperature as fallback
330+
331+
PlanetarySystem system = contract.getSystem();
332+
if (system == null) {
333+
LOGGER.warn("Contract {} has no system, using default temperature",
334+
contract.getName());
335+
return DEFAULT_TEMPERATURE;
336+
}
337+
338+
Planet planet = system.getPrimaryPlanet();
339+
if (planet == null) {
340+
LOGGER.warn("System {} has no primary planet, using default temperature",
341+
system.getName(campaign.getLocalDate()));
342+
return DEFAULT_TEMPERATURE;
343+
}
344+
345+
Integer temperature = planet.getTemperature(campaign.getLocalDate());
346+
if (temperature == null) {
347+
LOGGER.warn("Planet {} has no temperature data, using default temperature",
348+
planet.getName(campaign.getLocalDate()));
349+
return DEFAULT_TEMPERATURE;
350+
}
351+
352+
return temperature;
353+
}
354+
313355
/**
314356
* Set up initial state of a track, dimensions are based on number of assigned lances.
315357
*/

MekHQ/src/mekhq/campaign/stratCon/StratConRulesManager.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2019-2025 The MegaMek Team. All Rights Reserved.
2+
* Copyright (C) 2019-2026 The MegaMek Team. All Rights Reserved.
33
*
44
* This file is part of MekHQ.
55
*
@@ -832,9 +832,12 @@ public static void setScenarioParametersFromBiome(StratConTrackState track, Stra
832832
// if facility doesn't have a biome temp map or no entry for the current
833833
// temperature, use the default one
834834
if (facility.getBiomes().isEmpty() || (facility.getBiomeTempMap().floorEntry(kelvinTemp) == null)) {
835-
facilityBiome = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_FACILITY_BIOME)
836-
.floorEntry(kelvinTemp)
837-
.getValue();
835+
var defaultTempMap = biomeManifest.getTempMap(StratConBiomeManifest.TERRAN_FACILITY_BIOME);
836+
var biomeEntry = defaultTempMap.floorEntry(kelvinTemp);
837+
if (biomeEntry == null) {
838+
biomeEntry = defaultTempMap.firstEntry();
839+
}
840+
facilityBiome = biomeEntry.getValue();
838841
} else {
839842
facilityBiome = facility.getBiomeTempMap().floorEntry(kelvinTemp).getValue();
840843
}

0 commit comments

Comments
 (0)