001package com.fs.starfarer.api.impl.campaign.events; 002 003import java.awt.Color; 004import java.util.ArrayList; 005import java.util.Collections; 006import java.util.HashSet; 007import java.util.List; 008import java.util.Map; 009import java.util.Random; 010import java.util.Set; 011 012import org.apache.log4j.Logger; 013 014import com.fs.starfarer.api.Global; 015import com.fs.starfarer.api.campaign.CampaignClockAPI; 016import com.fs.starfarer.api.campaign.CampaignFleetAPI; 017import com.fs.starfarer.api.campaign.LocationAPI; 018import com.fs.starfarer.api.campaign.SectorEntityToken; 019import com.fs.starfarer.api.campaign.StarSystemAPI; 020import com.fs.starfarer.api.campaign.CargoAPI.CargoItemType; 021import com.fs.starfarer.api.campaign.econ.CommodityOnMarketAPI; 022import com.fs.starfarer.api.campaign.econ.MarketAPI; 023import com.fs.starfarer.api.campaign.events.CampaignEventTarget; 024import com.fs.starfarer.api.campaign.events.CampaignEventPlugin.PriceUpdatePlugin.PriceType; 025import com.fs.starfarer.api.impl.campaign.ids.Commodities; 026import com.fs.starfarer.api.impl.campaign.ids.Tags; 027import com.fs.starfarer.api.util.IntervalUtil; 028import com.fs.starfarer.api.util.SaveableIterator; 029import com.fs.starfarer.api.util.TimeoutTracker; 030import com.fs.starfarer.api.util.WeightedRandomPicker; 031 032public class TradeInfoUpdateEvent extends BaseEventPlugin { 033 034 public static final float TIMEOUT = 30f; 035 036 public static Logger log = Global.getLogger(TradeInfoUpdateEvent.class); 037 038 private IntervalUtil remoteTracker; 039 private IntervalUtil localTracker; 040 private TimeoutTracker<String> sinceLastLocalReport = new TimeoutTracker<String>(); 041 private TimeoutTracker<String> sinceLastRemoteReport = new TimeoutTracker<String>(); 042 043 private List<PriceUpdatePlugin> updatesForNextReport = new ArrayList<PriceUpdatePlugin>(); 044 private SectorEntityToken commRelayForNextReport = null; 045 046 public void init(String type, CampaignEventTarget eventTarget) { 047 super.init(type, eventTarget); 048 remoteTracker = new IntervalUtil(0.5f, 1.5f); 049 localTracker = new IntervalUtil(0.5f, 1.5f); 050 } 051 052 public void startEvent() { 053 super.startEvent(); 054 } 055 056 public void advance(float amount) { 057 //if (true) return; 058 059 if (!isEventStarted()) return; 060 if (isDone()) return; 061 062 float days = Global.getSector().getClock().convertToDays(amount); 063 064 sinceLastLocalReport.advance(days); 065 sinceLastRemoteReport.advance(days); 066 067 localTracker.advance(days); 068 if (localTracker.intervalElapsed()) { 069 checkLocalPrices(); 070 } 071 072 remoteTracker.advance(days); 073 if (remoteTracker.intervalElapsed()) { 074 checkRemotePrices(); 075 } 076 } 077 078 079 //private SaveableIterator<SectorEntityToken> relayIter = null; 080 private SaveableIterator<StarSystemAPI> starSystemIter = null; 081 082 private CampaignEventTarget tempTarget; 083 084 private void checkRemotePrices() { 085 086 if (starSystemIter == null || !starSystemIter.hasNext()) { 087// List<SectorEntityToken> relays = Global.getSector().getIntel().getCommSnifferLocations(); 088// if (Global.getSettings().isDevMode()) { 089// relays = Global.getSector().getEntitiesWithTag(Tags.COMM_RELAY); 090// } 091// relayIter = new SaveableIterator<SectorEntityToken>(relays); 092 List<StarSystemAPI> systems = new ArrayList<StarSystemAPI>(Global.getSector().getStarSystems()); 093 Collections.shuffle(systems); 094 starSystemIter = new SaveableIterator<StarSystemAPI>(systems); 095 096 float size = systems.size(); 097 float interval = 1.5f * TIMEOUT / size; 098 remoteTracker.setInterval(interval * 0.75f, interval * 1.25f); 099 } 100 if(!starSystemIter.hasNext()) return; 101 102 103 final StarSystemAPI system = starSystemIter.next(); 104 //if (Global.getSector().getCurrentLocation() == system) return; 105 106 107 List<SectorEntityToken> relays = system.getEntitiesWithTag(Tags.COMM_RELAY); 108 List<SectorEntityToken> withIntel = Global.getSector().getIntel().getCommSnifferLocations(); 109 110 SectorEntityToken relay = null; 111 for (SectorEntityToken curr : relays) { 112 if (withIntel.contains(curr)) { 113 relay = curr; 114 break; 115 } 116 } 117 118 boolean hasIntel = relay != null; 119 if (relay == null && relays.size() > 0) { 120 relay = relays.get(new Random().nextInt(relays.size())); 121 } 122 123 if (relay == null) return; 124 125 String id = relay.getContainingLocation().getId(); 126 if (sinceLastRemoteReport.contains(id)) return; 127 128 129 List<PriceUpdate> list = getPriceUpdatesFor(system); 130 if (!list.isEmpty()) { 131 132 commRelayForNextReport = relay; 133 if (hasIntel) { 134 pickUpdatesFrom(system, list, PickMode.REMOTE_WITH_INTEL); 135 if (!updatesForNextReport.isEmpty()) { 136 sinceLastRemoteReport.set(id, TIMEOUT); 137// Global.getSector().reportEventStage(this, "prices_sniffer", relay, MessagePriority.SECTOR, new BaseOnMessageDeliveryScript() { 138// public void beforeDelivery(CommMessageAPI message) { 139// if (system != Global.getSector().getPlayerFleet().getContainingLocation()) { 140// message.setShowInCampaignList(false); 141// } 142// } 143// }); 144 } 145 } else { 146 pickUpdatesFrom(system, list, PickMode.REMOTE); 147 if (!updatesForNextReport.isEmpty()) { 148 sinceLastRemoteReport.set(id, TIMEOUT); 149// Global.getSector().reportEventStage(this, "prices_remote", relay, MessagePriority.SECTOR, new BaseOnMessageDeliveryScript() { 150// public void beforeDelivery(CommMessageAPI message) { 151// if (system != Global.getSector().getPlayerFleet().getContainingLocation()) { 152// message.setShowInCampaignList(false); 153// } 154// } 155// }); 156 } 157 } 158 } 159 } 160 161 162 private void checkLocalPrices() { 163 //if (Global.getSector().isInNewGameAdvance()) return; 164 165 CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet(); 166 if (playerFleet.isInHyperspace() || playerFleet.getContainingLocation() == null) return; 167 String id = playerFleet.getContainingLocation().getId(); 168 if (sinceLastLocalReport.contains(id)) return; 169 170 List<PriceUpdate> list = getPriceUpdatesFor(playerFleet.getContainingLocation()); 171 172 if (!list.isEmpty()) { 173 pickUpdatesFrom(playerFleet.getContainingLocation(), list, PickMode.LOCAL); 174 if (!updatesForNextReport.isEmpty()) { 175 sinceLastLocalReport.set(id, TIMEOUT); 176 //Global.getSector().reportEventStage(this, "prices_local", playerFleet, MessagePriority.SECTOR); 177 } 178 } 179 } 180 181 private static enum PickMode { 182 LOCAL, 183 REMOTE, 184 REMOTE_WITH_INTEL, 185 } 186 187 private void pickUpdatesFrom(LocationAPI system, List<PriceUpdate> updates, PickMode mode) { 188 189 float numMarkets = 0; 190 for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) { 191 if (market.getContainingLocation() != system) continue; 192 //numMarkets += market.getSize(); 193 numMarkets++; 194 195 } 196 int max = 0; 197 switch (mode) { 198 case LOCAL: 199 //max = (int) Math.max(2, numMarkets - 2); 200 max = (int) Math.max(2, numMarkets) + 1; 201 break; 202 case REMOTE: 203 max = (int) Math.max(1, numMarkets - 2); 204 break; 205 case REMOTE_WITH_INTEL: 206 max = (int) Math.max(1, numMarkets); 207 break; 208 } 209 210 if (max > 5) max = 5; 211 if (max < 1) max = 1; 212 213 214 log.info(""); 215 log.info(""); 216 log.info("Picking " + max + " updates"); 217 218 WeightedRandomPicker<PriceUpdate> picker = new WeightedRandomPicker<PriceUpdate>(); 219 List<PriceUpdatePlugin> known = getPlayerKnownUpdates(); 220 for (PriceUpdate pu : updates) { 221 float weight = getWeightFor(pu, known); 222 if (weight <= 0) continue; 223 224 log.info(pu.getCommodity().getCommodity().getName() + "(" + pu.getCommodity().getMarket().getName() + "): weight " + weight); 225 picker.add(pu, weight); 226 } 227 228 log.info(""); 229 updatesForNextReport.clear(); 230 for (int i = 0; i < max; i++) { 231 PriceUpdate update = picker.pick(); 232 if (update != null) { 233 log.info("Picked " + update.getCommodity().getCommodity().getName() + "(" + update.getCommodity().getMarket().getName() + ")"); 234 updatesForNextReport.add(update); 235 picker.remove(update); 236 } 237 } 238 } 239 240 241 private void pickAllRelevantFromMarket(MarketAPI market, List<PriceUpdate> updates) { 242 log.info("Picking market updates"); 243 244 updatesForNextReport.clear(); 245 List<PriceUpdatePlugin> known = getPlayerKnownUpdates(); 246 for (PriceUpdate update : updates) { 247// System.out.println("Checking for local update: " + update.getCommodity().getCommodity().getName()); 248// if (!update.isSignificant()) continue; 249// float weight = getWeightFor(update, known); 250// log.info(update.getCommodity().getCommodity().getName() + ": weight " + (int) weight); 251 //if (update.getType() != PriceType.NORMAL || weight > 100) { 252 if (shouldUpdateLocally(update, known)) { 253 log.info("Adding " + update.getCommodity().getCommodity().getName() + "(" + update.getCommodity().getMarket().getName() + ")"); 254 updatesForNextReport.add(update); 255 } 256 } 257 } 258 259 260 private List<PriceUpdate> getPriceUpdatesFor(LocationAPI system) { 261 List<PriceUpdate> updates = new ArrayList<PriceUpdate>(); 262 for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) { 263 if (market.getContainingLocation() != system) continue; 264 updates.addAll(getUpdatesFor(market)); 265 } 266 return updates; 267 } 268 269 private List<PriceUpdate> getUpdatesFor(MarketAPI market) { 270 List<PriceUpdate> updates = new ArrayList<PriceUpdate>(); 271 if (!market.isInEconomy()) { 272 return updates; 273 } 274 for (CommodityOnMarketAPI com : market.getAllCommodities()) { 275 if (com.isNonEcon()) continue; 276 if (com.isPersonnel()) continue; 277 float volumeFactor = com.getStockpile() + com.getDemand().getDemandValue(); 278 if (volumeFactor < 50) continue; 279 PriceUpdate update = new PriceUpdate(com); 280 if (update.isSignificant()) { 281 updates.add(update); 282 } 283 } 284 return updates; 285 } 286 287 private float getWeightFor(PriceUpdatePlugin update, List<PriceUpdatePlugin> known) { 288 CommodityOnMarketAPI com = update.getCommodity(); 289 MarketAPI market = update.getMarket(); 290 291// if (market.getId().contains("achaman") && com.getId().equals("food")) { 292// System.out.println("dfsdfefw2"); 293// } 294// if (com.getId().equals("food")) { 295// System.out.println("dfsdfefw2"); 296// } 297 298 float volumeFactor = com.getStockpile() + com.getDemand().getDemandValue(); 299 if (volumeFactor == 0) return 0f; 300 301 volumeFactor = (float) Math.sqrt(volumeFactor); 302 303 //volumeFactor *= com.getCommodity().getBasePrice(); 304 305 //volumeFactor *= (float) market.getSize(); 306 307 if (update.getType() == PriceType.NORMAL) { 308 //volumeFactor = (float) Math.sqrt(volumeFactor); 309 volumeFactor *= 0.25f; 310 } 311 312 float numCheap = 0; 313 float numExpensive = 0; 314 float numNormal = 0; 315 316 317 CampaignClockAPI clock = Global.getSector().getClock(); 318 float daysSinceLastSame = Float.MAX_VALUE; 319 320 int numSeenSkipped = 0; 321 Set<CommodityOnMarketAPI> seen = new HashSet<CommodityOnMarketAPI>(); 322 for (PriceUpdatePlugin curr : known) { 323 CommodityOnMarketAPI currCom = curr.getCommodity(); 324 if (currCom == null) continue; 325 if (seen.contains(currCom)) { 326 numSeenSkipped++; 327 continue; 328 } 329 seen.add(currCom); 330 if (!currCom.getId().equals(com.getId())) continue; 331 332 if (currCom == com) { 333 float priceDiff = Math.abs(curr.getDemandPrice() + curr.getSupplyPrice() - update.getDemandPrice() - update.getSupplyPrice()); 334 if (priceDiff < 0.2f * (curr.getDemandPrice() + curr.getSupplyPrice())) { 335 daysSinceLastSame = clock.getElapsedDaysSince(curr.getTimestamp()); 336 } 337 } 338 //clock.getElapsedDaysSince(-55661070348000L) 339 switch (curr.getType()) { 340 case CHEAP: 341 numCheap++; 342 break; 343 case EXPENSIVE: 344 numExpensive++; 345 break; 346 case NORMAL: 347 numNormal++; 348 break; 349 } 350 } 351 352 if (daysSinceLastSame < 30) return 0f; 353 354 if (update.getType() == PriceType.NORMAL) { 355 if (numExpensive + numCheap == 0) { 356 return 0f; 357 } 358 if (numCheap == 0 && update.getAvailable() <= 10) { 359 return 0f; 360 } 361 if (numExpensive == 0 && update.getDemand() <= 10) { 362 return 0f; 363 } 364 } 365 366 float total = numCheap + numExpensive + numNormal; 367 368 float weightMult = 1f; 369 if (total <= 0) total = 1f; 370 switch (update.getType()) { 371 case CHEAP: 372 weightMult = 1f + 3f * Math.max(0, numNormal + numExpensive - numCheap) / total; 373 break; 374 case EXPENSIVE: 375 weightMult = 1f + 3f * Math.max(0, numNormal + numCheap - numExpensive) / total; 376 377 if (!com.isFuel() && !com.isPersonnel() && !com.getId().equals(Commodities.SUPPLIES)) { 378 CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet(); 379 float f = playerFleet.getCargo().getQuantity(CargoItemType.RESOURCES, com.getId()) / Math.max(playerFleet.getCargo().getMaxCapacity(), 1); 380 weightMult *= (1f + 2f * f); 381 } 382 383 break; 384 case NORMAL: 385 weightMult = 1f + 1f * Math.max(0, numCheap + numExpensive - numNormal) / total; 386 break; 387 } 388 389 return volumeFactor * weightMult; 390 } 391 392 393 394 private boolean shouldUpdateLocally(PriceUpdate update, List<PriceUpdatePlugin> known) { 395// if (update.getCommodity().getId().equals(Commodities.LOBSTER)) { 396// System.out.println("wfwefweew"); 397// } 398 if (!update.isSignificant()) return false; 399 400 CommodityOnMarketAPI com = update.getCommodity(); 401 MarketAPI market = com.getMarket(); 402 StarSystemAPI system = market.getStarSystem(); 403 404// float numCheap = 0; 405// float numExpensive = 0; 406// float numNormal = 0; 407// float numLocalNormal = 0; 408 409 CampaignClockAPI clock = Global.getSector().getClock(); 410 float daysSinceLastSame = Float.MAX_VALUE; 411 412 int numSeenSkipped = 0; 413 Set<CommodityOnMarketAPI> seen = new HashSet<CommodityOnMarketAPI>(); 414 PriceUpdatePlugin mostRecent = null; 415 for (PriceUpdatePlugin curr : known) { 416 CommodityOnMarketAPI currCom = curr.getCommodity(); 417 if (currCom == null) continue; 418 if (seen.contains(currCom)) { 419 numSeenSkipped++; 420 continue; 421 } 422 seen.add(currCom); 423 if (!currCom.getId().equals(com.getId())) continue; 424 425 if (currCom == com) { 426 mostRecent = curr; 427 float priceDiff = Math.abs(curr.getDemandPrice() + curr.getSupplyPrice() - update.getDemandPrice() - update.getSupplyPrice()); 428 if (priceDiff < 0.2f * (curr.getDemandPrice() + curr.getSupplyPrice())) { 429 daysSinceLastSame = clock.getElapsedDaysSince(curr.getTimestamp()); 430 } 431 break; 432 } 433 //clock.getElapsedDaysSince(-55661070348000L) 434// switch (curr.getType()) { 435// case CHEAP: 436// numCheap++; 437// break; 438// case EXPENSIVE: 439// numExpensive++; 440// break; 441// case NORMAL: 442// numNormal++; 443// if (system != null && system == curr.getMarket().getStarSystem()) { 444// numLocalNormal++; 445// } 446// break; 447// } 448 } 449 450 if (daysSinceLastSame < 5) return false; 451 452 //boolean canSell = (int) Misc.getRounded(update.getAvailable()) >= 5; 453 454 if (update.getType() != PriceType.NORMAL) { 455 return true; 456 } 457 458 //if (mostRecent != null && mostRecent.getType() != PriceType.NORMAL && update.getType() == PriceType.NORMAL) { 459 if (mostRecent != null) { 460// if (mostRecent.getType() != PriceType.NORMAL && update.getType() == PriceType.NORMAL) { 461// update.get 462// } 463 return true; 464 } 465 466 return false; 467 468// CommodityStatTracker stats = SharedData.getData().getActivityTracker().getCommodityTracker(); 469// CommodityStats cs = stats.getStats(update.getCommodity().getId()); 470// 471// float numMarkets = Global.getSector().getEconomy().getMarketsCopy().size(); 472// if (numMarkets < 1) return false; // ??? no markets 473// 474// if (com.getAverageStockpileAfterDemand() > cs.getTotalStockpiles() * 4f / numMarkets) { 475// return true; 476// } 477// if (com.getDemand().getDemandValue() > cs.getTotalDemand() * 1f / numMarkets) { 478// return true; 479// } 480 481// if (update.getType() == PriceType.NORMAL) { 482// if (numExpensive + numCheap == 0) return false; 483// if (numExpensive > 0 && (numCheap > 0 || numLocalNormal > 0)) return false; 484// } 485// 486// return true; 487 } 488 489 490 /** 491 * Some of the updates may be contradictory, but since this is used for computing 492 * weights when picking which updates to send, it's probably good enough. 493 * @return 494 */ 495 private List<PriceUpdatePlugin> getPlayerKnownUpdates() { 496 497 CampaignClockAPI clock = Global.getSector().getClock(); 498 499 List<PriceUpdatePlugin> updates = new ArrayList<PriceUpdatePlugin>(); 500// for (CommMessageAPI message : Global.getSector().getIntel().getMessagesCopy()) { 501// if (clock.getElapsedDaysSince(message.getTimeSent()) > 30) continue; 502// if (!message.hasTag(Tags.REPORT_PRICES)) continue; 503// 504// List<PriceUpdatePlugin> list = message.getPriceUpdates(); 505// if (list == null || list.isEmpty()) continue; 506// 507//// if (message.getMarket() != null && message.getMarket().getId().contains("skathi")) { 508//// System.out.println("e23edfsdf"); 509//// } 510// for (PriceUpdatePlugin curr : list) { 511// updates.add(curr); 512// } 513// } 514// 515// Collections.sort(updates, new Comparator<PriceUpdatePlugin>() { 516// public int compare(PriceUpdatePlugin o1, PriceUpdatePlugin o2) { 517// long result = (o2.getTimestamp() - o1.getTimestamp()); 518// if (result > 0) 519// return 1; 520// if (result < 0) 521// return -1; 522// return 0; 523// } 524// }); 525 526 527 return updates; 528 } 529 530 531 @Override 532 public void reportPlayerOpenedMarket(MarketAPI market) { 533 getLocalUpdates(market); 534 } 535 536 @Override 537 public void reportPlayerClosedMarket(MarketAPI market) { 538 //market.removeCondition(Conditions.EVENT_TRADE_DISRUPTION); 539 //market.getCommodityData(Commodities.HEAVY_MACHINERY).getPlayerPriceMod().modifyMult("sdfsdfsd", 0.1f); 540 getLocalUpdates(market); 541 } 542 543 protected void getLocalUpdates(MarketAPI market) { 544// float days = SharedData.getData().getPlayerActivityTracker().getDaysSinceLastVisitTo(market); 545// if (days < 3) return; 546 547 CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet(); 548// if (playerFleet.isInHyperspace()) return; 549 550 List<PriceUpdate> list = getUpdatesFor(market); 551 tempTarget = new CampaignEventTarget(market); 552 this.market = market; 553 if (!list.isEmpty()) { 554 pickAllRelevantFromMarket(market, list); 555 if (!updatesForNextReport.isEmpty()) { 556 //Global.getSector().reportEventStage(this, "prices_market", playerFleet, MessagePriority.DELIVER_IMMEDIATELY); 557 } 558 } 559 tempTarget = null; 560 this.market = null; 561 } 562 563 564 565 @Override 566 public List<PriceUpdatePlugin> getPriceUpdates() { 567 return updatesForNextReport; 568 } 569 570 @Override 571 public List<String> getRelatedCommodities() { 572 return super.getRelatedCommodities(); 573 } 574 575 576 public Map<String, String> getTokenReplacements() { 577 Map<String, String> map = super.getTokenReplacements(); 578// CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet(); 579// if (playerFleet.isInHyperspace()) { 580// map.put("$fromSystem", "hyperspace"); 581// } else { 582// map.put("$fromSystem", ((StarSystemAPI)playerFleet.getContainingLocation()).getBaseName()); 583// } 584 585 if (commRelayForNextReport != null) { 586 map.put("$relayName", commRelayForNextReport.getName()); 587 if (commRelayForNextReport.isInHyperspace()) { 588 map.put("$fromSystem", "hyperspace"); 589 } else { 590 map.put("$fromSystem", ((StarSystemAPI)commRelayForNextReport.getContainingLocation()).getBaseName()); 591 } 592 } 593 594 List<PriceUpdatePlugin> updates = getPriceUpdates(); 595 if (updates != null && !updates.isEmpty()) { 596 String priceList = "Price information updated for: "; 597 for (PriceUpdatePlugin update : updates) { 598 CommodityOnMarketAPI com = update.getCommodity(); 599 priceList += com.getCommodity().getName() + " (" + com.getMarket().getName() + "), "; 600 } 601 priceList = priceList.substring(0, priceList.length() - 2); 602 priceList += "."; 603 map.put("$priceList", priceList); 604 } 605 return map; 606 } 607 608 @Override 609 public String[] getHighlights(String stageId) { 610// List<String> result = new ArrayList<String>(); 611// addTokensToList(result, "$neededFood"); 612// return result.toArray(new String[0]); 613 return null; 614 } 615 616 @Override 617 public Color[] getHighlightColors(String stageId) { 618 return super.getHighlightColors(stageId); 619 } 620 621 @Override 622 public CampaignEventTarget getEventTarget() { 623 if (tempTarget != null) return tempTarget; 624 return super.getEventTarget(); 625 } 626 627 public boolean isDone() { 628 return false; 629 } 630 631 632 @Override 633 public String getEventName() { 634 return "Trade info update"; // not used anywhere 635 } 636 637 @Override 638 public CampaignEventCategory getEventCategory() { 639 return CampaignEventCategory.DO_NOT_SHOW_IN_MESSAGE_FILTER; 640 } 641 642 public boolean showAllMessagesIfOngoing() { 643 return false; 644 } 645} 646 647 648 649 650 651 652 653 654 655