From 350a0cb90ab476c8001dea77ff5fa2524f388d94 Mon Sep 17 00:00:00 2001 From: yairm210 Date: Sun, 19 Jan 2025 13:03:04 +0200 Subject: [PATCH 1/4] chore: Split CityStateFunctions.cityStateAttacked into subfunctions --- .../diplomacy/CityStateFunctions.kt | 116 ++++++++++-------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt index 724831a09173e..e002998b5c6ea 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt @@ -626,90 +626,110 @@ class CityStateFunctions(val civInfo: Civilization) { } } // Others might become wary! - if (attacker.isMinorCivAggressor()) { - for (cityState in civInfo.gameInfo.getAliveCityStates()) { - if (cityState == civInfo) // Must be a different minor - continue - if (cityState.getAllyCiv() == attacker.civName) // Must not be allied to the attacker - continue - if (!cityState.knows(attacker)) // Must have met - continue - if (cityState.questManager.wantsDead(civInfo.civName)) // Must not want us dead - continue - - var probability: Int - if (attacker.isMinorCivWarmonger()) { - // High probability if very aggressive - probability = when (cityState.getProximity(attacker)) { - Proximity.Neighbors -> 100 - Proximity.Close -> 75 - Proximity.Far -> 50 - Proximity.Distant -> 25 - else -> 0 - } - } else { - // Lower probability if only somewhat aggressive - probability = when (cityState.getProximity(attacker)) { - Proximity.Neighbors -> 50 - Proximity.Close -> 20 - else -> 0 - } - } + if (attacker.isMinorCivAggressor()) makeOtherCityStatesWaryOfAttacker(attacker) + + triggerProtectorCivs(attacker) - // Higher probability if already at war - if (cityState.isAtWarWith(attacker)) - probability += 50 + // Even if we aren't *technically* protectors, we *can* still be pissed you attacked our allies + triggerAllyCivs(attacker) - if (Random.Default.nextInt(100) <= probability) { - cityState.getDiplomacyManager(attacker)!!.becomeWary() + // Set up war with major pseudo-quest + civInfo.questManager.wasAttackedBy(attacker) + civInfo.getDiplomacyManager(attacker)!!.setFlag(DiplomacyFlags.RecentlyAttacked, 2) // Reminder to ask for unit gifts in 2 turns + } + + private fun makeOtherCityStatesWaryOfAttacker(attacker: Civilization) { + for (cityState in civInfo.gameInfo.getAliveCityStates()) { + if (cityState == civInfo) // Must be a different minor + continue + if (cityState.getAllyCiv() == attacker.civName) // Must not be allied to the attacker + continue + if (!cityState.knows(attacker)) // Must have met + continue + if (cityState.questManager.wantsDead(civInfo.civName)) // Must not want us dead + continue + + var probability: Int = if (attacker.isMinorCivWarmonger()) { + // High probability if very aggressive + when (cityState.getProximity(attacker)) { + Proximity.Neighbors -> 100 + Proximity.Close -> 75 + Proximity.Far -> 50 + Proximity.Distant -> 25 + else -> 0 + } + } else { + // Lower probability if only somewhat aggressive + when (cityState.getProximity(attacker)) { + Proximity.Neighbors -> 50 + Proximity.Close -> 20 + else -> 0 } } + + // Higher probability if already at war + if (cityState.isAtWarWith(attacker)) + probability += 50 + + if (Random.nextInt(100) <= probability) { + cityState.getDiplomacyManager(attacker)!!.becomeWary() + } } + } + private fun triggerProtectorCivs(attacker: Civilization) { for (protector in civInfo.cityStateFunctions.getProtectorCivs()) { val protectorDiplomacy = protector.getDiplomacyManager(attacker) ?: continue // Who? if (protectorDiplomacy.hasModifier(DiplomaticModifiers.AttackedProtectedMinor) - && protectorDiplomacy.getFlag(DiplomacyFlags.RememberAttackedProtectedMinor) > 50) - protectorDiplomacy.addModifier(DiplomaticModifiers.AttackedProtectedMinor, -15f) // Penalty less severe for second offence + && protectorDiplomacy.getFlag(DiplomacyFlags.RememberAttackedProtectedMinor) > 50 + ) + protectorDiplomacy.addModifier( + DiplomaticModifiers.AttackedProtectedMinor, + -15f + ) // Penalty less severe for second offence else protectorDiplomacy.addModifier(DiplomaticModifiers.AttackedProtectedMinor, -20f) protectorDiplomacy.setFlag(DiplomacyFlags.RememberAttackedProtectedMinor, 75) // Reset their memory if (protector.playerType != PlayerType.Human) // Humans can have their own emotions - attacker.addNotification("[${protector.civName}] is upset that you attacked [${civInfo.civName}], whom they have pledged to protect!", - NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, protector.civName) + attacker.addNotification( + "[${protector.civName}] is upset that you attacked [${civInfo.civName}], whom they have pledged to protect!", + NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, protector.civName + ) else // Let humans choose who to side with protector.popupAlerts.add( PopupAlert( AlertType.AttackedProtectedMinor, - attacker.civName + "@" + civInfo.civName) + attacker.civName + "@" + civInfo.civName + ) ) // we need to pass both civs as argument, hence the horrible chimera } + } - // Even if we aren't *technically* protectors, we *can* still be pissed you attacked our allies* + private fun triggerAllyCivs(attacker: Civilization) { val allyCivName = civInfo.getAllyCiv() val allyCiv = if (allyCivName != null) civInfo.gameInfo.getCivilization(allyCivName) else null - if (allyCiv != null && allyCiv !in civInfo.cityStateFunctions.getProtectorCivs() && allyCiv.knows(attacker)){ + if (allyCiv != null && allyCiv !in civInfo.cityStateFunctions.getProtectorCivs() && allyCiv.knows(attacker)) { val allyDiplomacy = allyCiv.getDiplomacyManager(attacker)!! // Less than if we were protectors allyDiplomacy.addModifier(DiplomaticModifiers.AttackedAlliedMinor, -10f) if (allyCiv.playerType != PlayerType.Human) // Humans can have their own emotions - attacker.addNotification("[${allyCiv.civName}] is upset that you attacked [${civInfo.civName}], whom they are allied with!", - NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, allyCiv.civName) + attacker.addNotification( + "[${allyCiv.civName}] is upset that you attacked [${civInfo.civName}], whom they are allied with!", + NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, allyCiv.civName + ) else // Let humans choose who to side with allyCiv.popupAlerts.add( PopupAlert( AlertType.AttackedAllyMinor, - attacker.civName + "@" + civInfo.civName) + attacker.civName + "@" + civInfo.civName + ) ) } - - // Set up war with major pseudo-quest - civInfo.questManager.wasAttackedBy(attacker) - civInfo.getDiplomacyManager(attacker)!!.setFlag(DiplomacyFlags.RecentlyAttacked, 2) // Reminder to ask for unit gifts in 2 turns } + /** A city state was destroyed. Its protectors are going to be upset! */ fun cityStateDestroyed(attacker: Civilization) { if (!civInfo.isCityState) return // What are we doing here? From 678df502f95adc77a41a6ccb6865f5d020ca639c Mon Sep 17 00:00:00 2001 From: yairm210 Date: Sun, 19 Jan 2025 13:07:46 +0200 Subject: [PATCH 2/4] chore: Simplified updateAllyForCityState --- .../diplomacy/CityStateFunctions.kt | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt index e002998b5c6ea..2c2428202ce09 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/CityStateFunctions.kt @@ -276,6 +276,7 @@ class CityStateFunctions(val civInfo: Civilization) { fun updateAllyCivForCityState() { var newAllyName: String? = null if (!civInfo.isCityState) return + val maxInfluence = civInfo.diplomacy .filter { it.value.otherCiv().isMajorCiv() && !it.value.otherCiv().isDefeated() } .maxByOrNull { it.value.getInfluence() } @@ -283,48 +284,49 @@ class CityStateFunctions(val civInfo: Civilization) { newAllyName = maxInfluence.key } - if (civInfo.getAllyCiv() != newAllyName) { - val oldAllyName = civInfo.getAllyCiv() - civInfo.setAllyCiv(newAllyName) - - if (newAllyName != null) { - val newAllyCiv = civInfo.gameInfo.getCivilization(newAllyName) - val text = "We have allied with [${civInfo.civName}]." - newAllyCiv.addNotification(text, - getNotificationActions(), - NotificationCategory.Diplomacy, civInfo.civName, - NotificationIcon.Diplomacy - ) - newAllyCiv.cache.updateViewableTiles() - newAllyCiv.cache.updateCivResources() - for (unique in newAllyCiv.getMatchingUniques(UniqueType.CityStateCanBeBoughtForGold) + newAllyCiv.getMatchingUniques(UniqueType.CityStateCanBeBoughtForGoldOld)) - newAllyCiv.getDiplomacyManager(civInfo)!!.setFlag(DiplomacyFlags.MarriageCooldown, unique.params[0].toInt()) - - // Join the wars of our new ally - loop through all civs they are at war with - for (newEnemy in civInfo.gameInfo.civilizations.filter { it.isAtWarWith(newAllyCiv) && it.isAlive() } ) { - if (!civInfo.isAtWarWith(newEnemy)) { - if (!civInfo.knows(newEnemy)) - // We have to meet first - civInfo.diplomacyFunctions.makeCivilizationsMeet(newEnemy, warOnContact = true) - civInfo.getDiplomacyManager(newEnemy)!!.declareWar(DeclareWarReason(WarType.CityStateAllianceWar, newAllyCiv)) - } + if (civInfo.getAllyCiv() == newAllyName) return + + val oldAllyName = civInfo.getAllyCiv() + civInfo.setAllyCiv(newAllyName) + + if (newAllyName != null) { + val newAllyCiv = civInfo.gameInfo.getCivilization(newAllyName) + val text = "We have allied with [${civInfo.civName}]." + newAllyCiv.addNotification(text, + getNotificationActions(), + NotificationCategory.Diplomacy, civInfo.civName, + NotificationIcon.Diplomacy + ) + newAllyCiv.cache.updateViewableTiles() + newAllyCiv.cache.updateCivResources() + for (unique in newAllyCiv.getMatchingUniques(UniqueType.CityStateCanBeBoughtForGold) + newAllyCiv.getMatchingUniques(UniqueType.CityStateCanBeBoughtForGoldOld)) + newAllyCiv.getDiplomacyManager(civInfo)!!.setFlag(DiplomacyFlags.MarriageCooldown, unique.params[0].toInt()) + + // Join the wars of our new ally - loop through all civs they are at war with + for (newEnemy in civInfo.gameInfo.civilizations.filter { it.isAtWarWith(newAllyCiv) && it.isAlive() } ) { + if (!civInfo.isAtWarWith(newEnemy)) { + if (!civInfo.knows(newEnemy)) + // We have to meet first (meet interesting people - and kill them!) + civInfo.diplomacyFunctions.makeCivilizationsMeet(newEnemy, warOnContact = true) + civInfo.getDiplomacyManager(newEnemy)!!.declareWar(DeclareWarReason(WarType.CityStateAllianceWar, newAllyCiv)) } } - if (oldAllyName != null && civInfo.isAlive()) { - val oldAllyCiv = civInfo.gameInfo.getCivilization(oldAllyName) - val text = "We have lost alliance with [${civInfo.civName}]." - oldAllyCiv.addNotification(text, - getNotificationActions(), - NotificationCategory.Diplomacy, civInfo.civName, - NotificationIcon.Diplomacy - ) - if (newAllyName != null && oldAllyCiv.knows(newAllyName)){ - val diplomacyManager = oldAllyCiv.getDiplomacyManager(newAllyName)!! - diplomacyManager.addModifier(DiplomaticModifiers.StoleOurAlly, -10f) - } - oldAllyCiv.cache.updateViewableTiles() - oldAllyCiv.cache.updateCivResources() + } + + if (oldAllyName != null && civInfo.isAlive()) { + val oldAllyCiv = civInfo.gameInfo.getCivilization(oldAllyName) + val text = "We have lost alliance with [${civInfo.civName}]." + oldAllyCiv.addNotification(text, + getNotificationActions(), + NotificationCategory.Diplomacy, civInfo.civName, + NotificationIcon.Diplomacy + ) + if (newAllyName != null && oldAllyCiv.knows(newAllyName)){ + val diplomacyManager = oldAllyCiv.getDiplomacyManager(newAllyName)!! + diplomacyManager.addModifier(DiplomaticModifiers.StoleOurAlly, -10f) } + oldAllyCiv.cache.updateViewableTiles() + oldAllyCiv.cache.updateCivResources() } } From 159f2e54d4dbb3d8336009258aade736c90dcde7 Mon Sep 17 00:00:00 2001 From: yairm210 Date: Sun, 19 Jan 2025 21:44:07 +0200 Subject: [PATCH 3/4] chore: Simplified validateInProgressConstructions --- .../com/unciv/logic/city/CityConstructions.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index bfcc4942f4f1b..b6e14e8124639 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -402,30 +402,30 @@ class CityConstructions : IsPartOfGameInfoSerialization { val rejectionReasons = (construction as INonPerpetualConstruction).getRejectionReasons(this) - if (rejectionReasons.any { it.hasAReasonToBeRemovedFromQueue() }) { - val workDone = getWorkDone(constructionName) - if (construction is Building) { - // Production put into wonders gets refunded - if (construction.isWonder && workDone != 0) { - city.civ.addGold(workDone) - city.civ.addNotification( - "Excess production for [$constructionName] converted to [$workDone] gold", - city.location, - NotificationCategory.Production, - NotificationIcon.Gold, "BuildingIcons/${constructionName}") - } - } else if (construction is BaseUnit) { - // Production put into upgradable units gets put into upgraded version - val cheapestUpgradeUnit = construction.getRulesetUpgradeUnits(city.state) - .map { city.civ.getEquivalentUnit(it) } - .filter { it.isBuildable(this) } - .minByOrNull { it.cost } - if (rejectionReasons.all { it.type == RejectionReasonType.Obsoleted } && cheapestUpgradeUnit != null) { - inProgressConstructions[cheapestUpgradeUnit.name] = (inProgressConstructions[cheapestUpgradeUnit.name] ?: 0) + workDone - } + if (!rejectionReasons.any { it.hasAReasonToBeRemovedFromQueue() }) continue + + val workDone = getWorkDone(constructionName) + if (construction is Building) { + // Production put into wonders gets refunded + if (construction.isWonder && workDone != 0) { + city.civ.addGold(workDone) + city.civ.addNotification( + "Excess production for [$constructionName] converted to [$workDone] gold", + city.location, + NotificationCategory.Production, + NotificationIcon.Gold, "BuildingIcons/${constructionName}") + } + } else if (construction is BaseUnit) { + // Production put into upgradable units gets put into upgraded version + val cheapestUpgradeUnit = construction.getRulesetUpgradeUnits(city.state) + .map { city.civ.getEquivalentUnit(it) } + .filter { it.isBuildable(this) } + .minByOrNull { it.cost } + if (rejectionReasons.all { it.type == RejectionReasonType.Obsoleted } && cheapestUpgradeUnit != null) { + inProgressConstructions[cheapestUpgradeUnit.name] = (inProgressConstructions[cheapestUpgradeUnit.name] ?: 0) + workDone } - inProgressConstructions.remove(constructionName) } + inProgressConstructions.remove(constructionName) } } From 2f79658758315154eb9a107c18338d1cb578748c Mon Sep 17 00:00:00 2001 From: yairm210 Date: Tue, 21 Jan 2025 12:13:34 +0200 Subject: [PATCH 4/4] Multiplayer screen correctly handles errors when downloading mods --- .../multiplayerscreens/MultiplayerScreen.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/src/com/unciv/ui/screens/multiplayerscreens/MultiplayerScreen.kt b/core/src/com/unciv/ui/screens/multiplayerscreens/MultiplayerScreen.kt index 47a39a17c1e35..82a6bc32d4e8d 100644 --- a/core/src/com/unciv/ui/screens/multiplayerscreens/MultiplayerScreen.kt +++ b/core/src/com/unciv/ui/screens/multiplayerscreens/MultiplayerScreen.kt @@ -81,13 +81,18 @@ class MultiplayerScreen : PickerScreen() { // Download missing mods Concurrency.runOnNonDaemonThreadPool(LoadGameScreen.downloadMissingMods) { - LoadGameScreen.loadMissingMods(missingMods, onModDownloaded = { - Concurrency.runOnGLThread { ToastPopup("[$it] Downloaded!", this@MultiplayerScreen) } - }, - onCompleted = { - RulesetCache.loadRulesets() - Concurrency.runOnGLThread { MultiplayerHelpers.loadMultiplayerGame(this@MultiplayerScreen, selectedGame!!) } - }) + try { + LoadGameScreen.loadMissingMods(missingMods, onModDownloaded = { + Concurrency.runOnGLThread { ToastPopup("[$it] Downloaded!", this@MultiplayerScreen) } + }, + onCompleted = { + RulesetCache.loadRulesets() + Concurrency.runOnGLThread { MultiplayerHelpers.loadMultiplayerGame(this@MultiplayerScreen, selectedGame!!) } + }) + } catch (ex: Exception) { + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) + launchOnGLThread { ToastPopup(message, this@MultiplayerScreen) } + } } } }