001package com.fs.starfarer.api.impl.campaign.intel.punitive; 002 003import java.util.ArrayList; 004import java.util.HashSet; 005import java.util.LinkedHashMap; 006import java.util.List; 007import java.util.Map; 008import java.util.Random; 009import java.util.Set; 010 011import org.json.JSONObject; 012 013import com.fs.starfarer.api.EveryFrameScript; 014import com.fs.starfarer.api.Global; 015import com.fs.starfarer.api.campaign.FactionAPI; 016import com.fs.starfarer.api.campaign.econ.CommodityMarketDataAPI; 017import com.fs.starfarer.api.campaign.econ.CommodityOnMarketAPI; 018import com.fs.starfarer.api.campaign.econ.Industry; 019import com.fs.starfarer.api.campaign.econ.MarketAPI; 020import com.fs.starfarer.api.impl.campaign.DebugFlags; 021import com.fs.starfarer.api.impl.campaign.ids.Factions; 022import com.fs.starfarer.api.impl.campaign.ids.Industries; 023import com.fs.starfarer.api.impl.campaign.ids.MemFlags; 024import com.fs.starfarer.api.impl.campaign.intel.BaseIntelPlugin; 025import com.fs.starfarer.api.impl.campaign.rulecmd.salvage.MarketCMD; 026import com.fs.starfarer.api.util.IntervalUtil; 027import com.fs.starfarer.api.util.Misc; 028import com.fs.starfarer.api.util.WeightedRandomPicker; 029 030public class PunitiveExpeditionManager implements EveryFrameScript { 031 032 public static final String KEY = "$core_punitiveExpeditionManager"; 033 public static PunitiveExpeditionManager getInstance() { 034 Object test = Global.getSector().getMemoryWithoutUpdate().get(KEY); 035 return (PunitiveExpeditionManager) test; 036 } 037 038 public static int MAX_CONCURRENT = Global.getSettings().getInt("punExMaxConcurrent"); 039 public static float PROB_TIMEOUT_PER_SENT = Global.getSettings().getFloat("punExProbTimeoutPerExpedition"); 040 public static float MIN_TIMEOUT = Global.getSettings().getFloatFromArray("punExTimeoutDays", 0); 041 public static float MAX_TIMEOUT = Global.getSettings().getFloatFromArray("punExTimeoutDays", 1); 042 043 public static int MIN_COLONY_SIZE_FOR_NON_TERRITORIAL = Global.getSettings().getInt("punExMinColonySizeForNonTerritorial"); 044 045 046 // if more factions send non-territorial expeditions, longer timeout 047 public static float TARGET_NUMBER_FOR_FREQUENCY = 5f; 048 049 public static float ANGER_BUILDUP_MULT = 0.5f; 050 051 public static int FACTION_MUST_BE_IN_TOP_X_PRODUCERS = 3; 052 //public static float PLAYER_FRACTION_TO_NOTICE = 0.33f; 053 public static float PLAYER_FRACTION_TO_NOTICE = 0.5f; 054 //public static final float MAX_THRESHOLD = 1000f; 055 public static float MAX_THRESHOLD = 600f; 056 057 public static enum PunExType { 058 ANTI_COMPETITION, 059 ANTI_FREE_PORT, 060 TERRITORIAL, 061 } 062 063 public static enum PunExGoal { 064 RAID_PRODUCTION, 065 RAID_SPACEPORT, 066 BOMBARD, 067 //EVACUATE, 068 } 069 070 public static class PunExReason { 071 public PunExType type; 072 public String commodityId; 073 public String marketId; 074 public float weight; 075 public PunExReason(PunExType type) { 076 this.type = type; 077 } 078 } 079 080 public static class PunExData { 081 public FactionAPI faction; 082 public IntervalUtil tracker = new IntervalUtil(20f, 40f); 083 public float anger = 0f; 084 public float threshold = 100f; 085 public float timeout = 0f;; 086 public BaseIntelPlugin intel; 087 public Random random = new Random(); 088 089 public int numSuccesses = 0; 090 public int numAttempts = 0; 091 } 092 093 protected float timeout = 0f; 094 protected int numSentSinceTimeout = 0; 095 protected LinkedHashMap<FactionAPI, PunExData> data = new LinkedHashMap<FactionAPI, PunExData>(); 096 097 public PunitiveExpeditionManager() { 098 Global.getSector().getMemoryWithoutUpdate().set(KEY, this); 099 } 100 101 protected Object readResolve() { 102 return this; 103 } 104 105 public PunExData getDataFor(FactionAPI faction) { 106 return data.get(faction); 107 } 108 109 110 public LinkedHashMap<FactionAPI, PunExData> getData() { 111 return data; 112 } 113 114 public void advance(float amount) { 115 //if (true) return; 116 117 float days = Misc.getDays(amount); 118 119 Set<FactionAPI> seen = new HashSet<FactionAPI>(); 120 for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) { 121// JSONObject json = market.getFaction().getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA); 122// boolean canSendWithoutMilitaryBase = json != null && json.optBoolean("canSendWithoutMilitaryBase", false); 123 //if (market.getMemoryWithoutUpdate().getBoolean(MemFlags.MARKET_MILITARY) || canSendWithoutMilitaryBase) { 124 if (true) { 125 FactionAPI faction = market.getFaction(); 126 if (Misc.getCommissionFaction() == faction) continue; 127 128 if (seen.contains(faction) || data.containsKey(faction)) { 129 seen.add(faction); 130 continue; 131 } 132 JSONObject json = faction.getCustomJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA); 133 if (json != null) { 134 PunExData curr = new PunExData(); 135 curr.faction = faction; 136 data.put(faction, curr); 137 seen.add(faction); 138 } 139 } 140 } 141 data.keySet().retainAll(seen); 142 143 if (timeout > 0) { 144 timeout -= days * (DebugFlags.PUNITIVE_EXPEDITION_DEBUG ? 1000f : 1f); 145 if (timeout <= 0) { 146 timeout = 0; 147 numSentSinceTimeout = 0; 148 } 149 return; 150 } 151 152 boolean first = true; 153 for (PunExData curr : data.values()) { 154 if (first && DebugFlags.PUNITIVE_EXPEDITION_DEBUG) { 155 days *= 1000f; 156 curr.timeout = 0f; 157 curr.anger = 1000f; 158 } 159 first = false; 160 161 if (curr.intel != null) { 162 if (curr.intel.isEnded()) { 163 curr.timeout = 100f + 100f * curr.random.nextFloat(); 164 165 if (curr.intel instanceof PunitiveExpeditionIntel) { 166 PunitiveExpeditionIntel intel = (PunitiveExpeditionIntel) curr.intel; 167 if (!intel.isTerritorial()) { 168 curr.timeout += getExtraTimeout(curr); 169 } 170 } 171 172 curr.intel = null; 173 } 174 } else { 175 curr.timeout -= days; 176 if (curr.timeout <= 0) curr.timeout = 0; 177 } 178 179 180 curr.tracker.advance(days); 181 //System.out.println(curr.tracker.getElapsed()); 182 if (curr.tracker.intervalElapsed() && 183 curr.intel == null && 184 curr.timeout <= 0) { 185 checkExpedition(curr); 186 } 187 } 188 } 189 190 public float getExtraTimeout(PunExData d) { 191 float total = 0f; 192 for (PunExData curr : data.values()) { 193 JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA); 194 if (json == null) continue; 195 196 List<MarketAPI> markets = Misc.getFactionMarkets(curr.faction, null); 197 if (markets.isEmpty()) continue; 198 199 boolean vsCompetitors = json.optBoolean("vsCompetitors", false); 200 boolean vsFreePort = json.optBoolean("vsFreePort", false); 201 boolean territorial = json.optBoolean("territorial", false); 202 203 if (vsCompetitors || vsFreePort) { 204 total++; 205 } 206 } 207 208 return Math.min(10f, Math.max(0, total - TARGET_NUMBER_FOR_FREQUENCY)) * 209 (MIN_TIMEOUT * 0.9f + MIN_TIMEOUT * 0.9f * d.random.nextFloat()); 210 } 211 212 213 public int getOngoing() { 214 int ongoing = 0; 215 for (PunExData d : data.values()) { 216 if (d.intel != null) { 217 ongoing++; 218 } 219 } 220 //ongoing = 0; 221 return ongoing; 222 } 223 224 protected void checkExpedition(PunExData curr) { 225 JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA); 226 if (json == null) return; 227 228// if (curr.faction.getId().equals(Factions.TRITACHYON)) { 229// System.out.println("wefwefwe"); 230// } 231 List<PunExReason> reasons = getExpeditionReasons(curr); 232// if (!reasons.isEmpty()) { 233// System.out.println("HERE"); 234// } 235 float total = 0f; 236 for (PunExReason reason : reasons) { 237 total += reason.weight; 238 } 239 240 total *= ANGER_BUILDUP_MULT; 241 242 curr.anger += total * (0.25f + curr.random.nextFloat() * 0.75f); 243 if (curr.anger >= curr.threshold) { 244 if (getOngoing() >= MAX_CONCURRENT) { 245 curr.anger = 0; 246 } else { 247 createExpedition(curr); 248 } 249 } 250 } 251 252 public static float COMPETITION_PRODUCTION_MULT = 20f; 253 public static float ILLEGAL_GOODS_MULT = 3f; 254 public static float FREE_PORT_SIZE_MULT = 5f; 255 public static float TERRITORIAL_ANGER = 500f; 256 257 public List<PunExReason> getExpeditionReasons(PunExData curr) { 258 List<PunExReason> result = new ArrayList<PunExReason>(); 259 260 JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA); 261 if (json == null) return result; 262 263 List<MarketAPI> markets = Misc.getFactionMarkets(curr.faction, null); 264 if (markets.isEmpty()) return result; 265 266 boolean vsCompetitors = json.optBoolean("vsCompetitors", false); 267 boolean vsFreePort = json.optBoolean("vsFreePort", false); 268 boolean territorial = json.optBoolean("territorial", false); 269 270 MarketAPI test = markets.get(0); 271 FactionAPI player = Global.getSector().getPlayerFaction(); 272 273 if (vsCompetitors) { 274 for (CommodityOnMarketAPI com : test.getAllCommodities()) { 275 if (com.isNonEcon()) continue; 276 if (curr.faction.isIllegal(com.getId())) continue; 277 278 CommodityMarketDataAPI cmd = com.getCommodityMarketData(); 279 if (cmd.getMarketValue() <= 0) continue; 280 281 Map<FactionAPI, Integer> shares = cmd.getMarketSharePercentPerFaction(); 282 int numHigher = 0; 283 int factionShare = shares.get(curr.faction); 284 if (factionShare <= 0) continue; 285 286 for (FactionAPI faction : shares.keySet()) { 287 if (curr.faction == faction) continue; 288 if (shares.get(faction) > factionShare) { 289 numHigher++; 290 } 291 } 292 293 if (numHigher >= FACTION_MUST_BE_IN_TOP_X_PRODUCERS) continue; 294 295 int playerShare = cmd.getMarketSharePercent(player); 296 float threshold = PLAYER_FRACTION_TO_NOTICE; 297 if (DebugFlags.PUNITIVE_EXPEDITION_DEBUG) { 298 threshold = 0.1f; 299 } 300 if (playerShare < factionShare * threshold || playerShare <= 0) continue; 301 302 PunExReason reason = new PunExReason(PunExType.ANTI_COMPETITION); 303 reason.weight = (float)playerShare / (float)factionShare * COMPETITION_PRODUCTION_MULT; 304 reason.commodityId = com.getId(); 305 result.add(reason); 306 } 307 } 308 309 if (vsFreePort) { 310 for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) { 311 if (!market.isPlayerOwned()) continue; 312 if (!market.isFreePort()) continue; 313 if (market.isInHyperspace()) continue; 314 315 for (CommodityOnMarketAPI com : test.getAllCommodities()) { 316 if (com.isNonEcon()) continue; 317 if (!curr.faction.isIllegal(com.getId())) continue; 318 319 CommodityMarketDataAPI cmd = com.getCommodityMarketData(); 320 if (cmd.getMarketValue() <= 0) continue; 321 322 int playerShare = cmd.getMarketSharePercent(player); 323 if (playerShare <= 0) continue; 324 325 PunExReason reason = new PunExReason(PunExType.ANTI_FREE_PORT); 326 reason.weight = playerShare * ILLEGAL_GOODS_MULT; 327 reason.commodityId = com.getId(); 328 reason.marketId = market.getId(); 329 result.add(reason); 330 } 331 332 if (market.isFreePort()) { 333 PunExReason reason = new PunExReason(PunExType.ANTI_FREE_PORT); 334 reason.weight = Math.max(1, market.getSize() - 2) * FREE_PORT_SIZE_MULT; 335 reason.marketId = market.getId(); 336 result.add(reason); 337 } 338 } 339 } 340 341 if (territorial) { 342 int maxSize = MarketCMD.getBombardDestroyThreshold(); 343 for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) { 344 if (!market.isPlayerOwned()) continue; 345 if (market.isInHyperspace()) continue; 346 347 boolean destroy = market.getSize() <= maxSize; 348 if (!destroy) continue; 349 350 FactionAPI claimedBy = Misc.getClaimingFaction(market.getPrimaryEntity()); 351 if (claimedBy != curr.faction) continue; 352 353 PunExReason reason = new PunExReason(PunExType.TERRITORIAL); 354 reason.weight = TERRITORIAL_ANGER; 355 reason.marketId = market.getId(); 356 result.add(reason); 357 } 358 } 359 360 return result; 361 } 362 363 364 public void createExpedition(PunExData curr) { 365 createExpedition(curr, null); 366 } 367 public void createExpedition(PunExData curr, Integer fpOverride) { 368 369 JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA); 370 if (json == null) return; 371 372// boolean vsCompetitors = json.optBoolean("vsCompetitors", false); 373// boolean vsFreePort = json.optBoolean("vsFreePort", false); 374 boolean canBombard = json.optBoolean("canBombard", false); 375// boolean territorial = json.optBoolean("territorial", false); 376 377 List<PunExReason> reasons = getExpeditionReasons(curr); 378 WeightedRandomPicker<PunExReason> reasonPicker = new WeightedRandomPicker<PunExReason>(curr.random); 379 for (PunExReason r : reasons) { 380 //if (r.type == PunExType.ANTI_COMPETITION) continue; 381 reasonPicker.add(r, r.weight); 382 } 383 PunExReason reason = reasonPicker.pick(); 384 if (reason == null) return; 385 386 387 WeightedRandomPicker<MarketAPI> targetPicker = new WeightedRandomPicker<MarketAPI>(curr.random); 388 //for (PunExReason reason : reasons) { 389 390 //WeightedRandomPicker<MarketAPI> picker = new WeightedRandomPicker<MarketAPI>(curr.random); 391 for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) { 392 if (!market.isPlayerOwned()) continue; 393 if (market.isInHyperspace()) continue; 394 395 float weight = 0f; 396 if (reason.type == PunExType.ANTI_COMPETITION && reason.commodityId != null) { 397 if (market.getSize() < MIN_COLONY_SIZE_FOR_NON_TERRITORIAL) continue; 398 399 CommodityOnMarketAPI com = market.getCommodityData(reason.commodityId); 400 int share = com.getCommodityMarketData().getExportMarketSharePercent(market); 401// if (share <= 0 && com.getAvailable() > 0) { 402// share = 1; 403// } 404 weight += share * share; 405 } else if (reason.type == PunExType.ANTI_FREE_PORT && market.getId().equals(reason.marketId)) { 406 if (market.getSize() < MIN_COLONY_SIZE_FOR_NON_TERRITORIAL) continue; 407 408 weight = 1f; 409 } else if (reason.type == PunExType.TERRITORIAL && market.getId().equals(reason.marketId)) { 410 weight = 1f; 411 } 412 413 targetPicker.add(market, weight); 414 } 415 416 MarketAPI target = targetPicker.pick(); 417 if (target == null) return; 418 419 WeightedRandomPicker<MarketAPI> picker = new WeightedRandomPicker<MarketAPI>(curr.random); 420 for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) { 421 boolean canSendWithoutMilitaryBase = json.optBoolean("canSendWithoutMilitaryBase", false); 422 boolean military = market.getMemoryWithoutUpdate().getBoolean(MemFlags.MARKET_MILITARY); 423 if (market.getFaction() == curr.faction && 424 (military || canSendWithoutMilitaryBase)) { 425 float w = 1f; 426 if (military) w *= 10f; 427 picker.add(market, market.getSize() * w); 428 } 429 } 430 431 MarketAPI from = picker.pick(); 432 if (from == null) return; 433 434 PunExGoal goal = null; 435 Industry industry = null; 436 if (reason.type == PunExType.ANTI_FREE_PORT) { 437 goal = PunExGoal.RAID_SPACEPORT; 438 if (canBombard && curr.numSuccesses >= 2) { 439 goal = PunExGoal.BOMBARD; 440 } 441 } else if (reason.type == PunExType.TERRITORIAL) { 442 if (canBombard || true) { 443 goal = PunExGoal.BOMBARD; 444 } else { 445 //goal = PunExGoal.EVACUATE; 446 } 447 } else { 448 goal = PunExGoal.RAID_PRODUCTION; 449 if (reason.commodityId == null || curr.numSuccesses >= 1) { 450 goal = PunExGoal.RAID_SPACEPORT; 451 } 452 if (canBombard && curr.numSuccesses >= 2) { 453 goal = PunExGoal.BOMBARD; 454 } 455 } 456 457 //goal = PunExGoal.BOMBARD; 458 459 if (goal == PunExGoal.RAID_SPACEPORT) { 460 for (Industry temp : target.getIndustries()) { 461 if (temp.getSpec().hasTag(Industries.TAG_UNRAIDABLE)) continue; 462 if (temp.getSpec().hasTag(Industries.TAG_SPACEPORT)) { 463 industry = temp; 464 break; 465 } 466 } 467 if (industry == null) return; 468 } else if (goal == PunExGoal.RAID_PRODUCTION && reason.commodityId != null) { 469 int max = 0; 470 for (Industry temp : target.getIndustries()) { 471 if (temp.getSpec().hasTag(Industries.TAG_UNRAIDABLE)) continue; 472 473 int prod = temp.getSupply(reason.commodityId).getQuantity().getModifiedInt(); 474 if (prod > max) { 475 max = prod; 476 industry = temp; 477 } 478 } 479 if (industry == null) return; 480 } 481 482 //float fp = from.getSize() * 20 + threshold * 0.5f; 483 float fp = 50 + curr.threshold * 0.5f; 484 fp = Math.max(50, fp - 50); 485 //fp = 500; 486// if (from.getFaction().isHostileTo(target.getFaction())) { 487// fp *= 1.25f; 488// } 489 490 if (fpOverride != null) { 491 fp = fpOverride; 492 } 493 494 495 float totalAttempts = 0f; 496 for (PunExData d : data.values()) { 497 totalAttempts += d.numAttempts; 498 } 499 //if (totalAttempts > 10) totalAttempts = 10; 500 501 float extraMult = 0f; 502 if (totalAttempts <= 2) { 503 extraMult = 0f; 504 } else if (totalAttempts <= 4) { 505 extraMult = 1f; 506 } else if (totalAttempts <= 7) { 507 extraMult = 2f; 508 } else if (totalAttempts <= 10) { 509 extraMult = 3f; 510 } else { 511 extraMult = 4f; 512 } 513 514 float orgDur = 20f + extraMult * 10f + (10f + extraMult * 5f) * (float) Math.random(); 515 516 517 curr.intel = new PunitiveExpeditionIntel(from.getFaction(), from, target, fp, orgDur, 518 goal, industry, reason); 519 if (curr.intel.isDone()) { 520 curr.intel = null; 521 timeout = orgDur + MIN_TIMEOUT + curr.random.nextFloat() * (MAX_TIMEOUT - MIN_TIMEOUT); 522 return; 523 } 524 525 if (curr.random.nextFloat() < numSentSinceTimeout * PROB_TIMEOUT_PER_SENT) { 526 timeout = orgDur + MIN_TIMEOUT + curr.random.nextFloat() * (MAX_TIMEOUT - MIN_TIMEOUT); 527 } 528 numSentSinceTimeout++; 529 530 curr.numAttempts++; 531 curr.anger = 0f; 532 curr.threshold *= 2f; 533 if (curr.threshold > MAX_THRESHOLD) { 534 curr.threshold = MAX_THRESHOLD; 535 } 536 } 537 538 539 540 public boolean isDone() { 541 return false; 542 } 543 544 public boolean runWhilePaused() { 545 return false; 546 } 547 548} 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563