001package com.fs.starfarer.api.impl.campaign.missions.hub; 002 003import java.util.ArrayList; 004import java.util.HashSet; 005import java.util.LinkedHashSet; 006import java.util.List; 007import java.util.Map; 008import java.util.Random; 009import java.util.Set; 010 011import org.lwjgl.util.vector.Vector2f; 012 013import com.fs.starfarer.api.Global; 014import com.fs.starfarer.api.campaign.InteractionDialogAPI; 015import com.fs.starfarer.api.campaign.SectorEntityToken; 016import com.fs.starfarer.api.campaign.StarSystemAPI; 017import com.fs.starfarer.api.campaign.comm.IntelInfoPlugin; 018import com.fs.starfarer.api.campaign.econ.MarketAPI; 019import com.fs.starfarer.api.campaign.rules.MemoryAPI; 020import com.fs.starfarer.api.characters.ImportantPeopleAPI; 021import com.fs.starfarer.api.characters.PersonAPI; 022import com.fs.starfarer.api.impl.campaign.DebugFlags; 023import com.fs.starfarer.api.impl.campaign.DevMenuOptions; 024import com.fs.starfarer.api.impl.campaign.ids.Tags; 025import com.fs.starfarer.api.impl.campaign.intel.bar.events.BarEventManager; 026import com.fs.starfarer.api.impl.campaign.intel.contacts.ContactIntel; 027import com.fs.starfarer.api.impl.campaign.missions.hub.HubMissionWithSearch.StarSystemUnexploredReq; 028import com.fs.starfarer.api.impl.campaign.rulecmd.CallEvent.CallableEvent; 029import com.fs.starfarer.api.impl.campaign.rulecmd.FireAll; 030import com.fs.starfarer.api.impl.campaign.rulecmd.FireBest; 031import com.fs.starfarer.api.loading.PersonMissionSpec; 032import com.fs.starfarer.api.util.Misc; 033import com.fs.starfarer.api.util.Misc.Token; 034import com.fs.starfarer.api.util.TimeoutTracker; 035import com.fs.starfarer.api.util.WeightedRandomPicker; 036 037public class BaseMissionHub implements MissionHub, CallableEvent { 038 039 public static float UPDATE_INTERVAL = Global.getSettings().getFloat("contactMissionUpdateIntervalDays"); 040 public static int MIN_TO_SHOW = Global.getSettings().getInt("contactMinMissions"); 041 public static int MAX_TO_SHOW = Global.getSettings().getInt("contactMaxMissions"); 042 public static int MAX_TO_SHOW_WITH_BONUS = Global.getSettings().getInt("contactMaxMissionsWithPriorityBonus"); 043 044 045 public static String CONTACT_SUSPENDED = "$mHub_contactSuspended"; 046 public static String NUM_BONUS_MISSIONS = "$mHub_numBonusMissions"; 047 public static String MISSION_QUALITY_BONUS = "$mHub_missionQualityBonus"; 048 public static String LAST_OPENED = "$mHub_lastOpenedTimestamp"; 049 050 public static String KEY = "$mHub"; 051 public static void set(PersonAPI person, MissionHub hub) { 052 if (hub == null) { 053 person.getMemoryWithoutUpdate().unset(KEY); 054 } else { 055 person.getMemoryWithoutUpdate().set(KEY, hub); 056 } 057 } 058 public static MissionHub get(PersonAPI person) { 059 if (person == null) return null; 060 if (person.getMemoryWithoutUpdate().contains(KEY)) { 061 return (MissionHub) person.getMemoryWithoutUpdate().get(KEY); 062 } 063 return null; 064 } 065 066 public static float getDaysSinceLastOpened(PersonAPI person) { 067 if (!person.getMemoryWithoutUpdate().contains(LAST_OPENED)) { 068 return -1; 069 } 070 long ts = person.getMemoryWithoutUpdate().getLong(LAST_OPENED); 071 return Global.getSector().getClock().getElapsedDaysSince(ts); 072 } 073 public static long getLastOpenedTimestamp(PersonAPI person) { 074 if (!person.getMemoryWithoutUpdate().contains(LAST_OPENED)) { 075 return Long.MIN_VALUE; 076 } 077 return person.getMemoryWithoutUpdate().getLong(LAST_OPENED); 078 } 079 080 public static void setDaysSinceLastOpened(PersonAPI person) { 081 person.getMemoryWithoutUpdate().set(LAST_OPENED, Global.getSector().getClock().getTimestamp()); 082 } 083 084 085 //protected List<MHMission> missions = new ArrayList<MHMission>(); 086 //protected TimeoutTracker<HubMissionCreator> timeout = new TimeoutTracker<HubMissionCreator>(); 087 088 protected TimeoutTracker<String> timeout = new TimeoutTracker<String>(); 089 protected TimeoutTracker<String> recentlyAcceptedTimeout = new TimeoutTracker<String>(); 090 protected List<HubMissionCreator> creators = new ArrayList<HubMissionCreator>(); 091 protected transient List<HubMission> offered = new ArrayList<HubMission>(); 092 093 protected PersonAPI person; 094 095 public BaseMissionHub(PersonAPI person) { 096 this.person = person; 097 098 //creators.add(new GADataFromRuinsCreator()); 099 readResolve(); 100 } 101 102 public void updateMissionCreatorsFromSpecs() { 103 List<PersonMissionSpec> specs = getMissionsForPerson(person); 104 Set<String> validMissions = new HashSet<String>(); 105 Set<String> alreadyHaveCreatorsFor = new HashSet<String>(); 106 for (PersonMissionSpec spec : specs) { 107 validMissions.add(spec.getMissionId()); 108 } 109 110 for (HubMissionCreator curr : creators) { 111 if (!curr.wasAutoAdded()) continue; 112 113 if (!validMissions.contains(curr.getSpecId())) { 114 curr.setActive(false); 115 //System.out.println("blahsdf"); 116 } else { 117 curr.setActive(true); 118 alreadyHaveCreatorsFor.add(curr.getSpecId()); 119 } 120 } 121 122 for (PersonMissionSpec spec : specs) { 123 if (!alreadyHaveCreatorsFor.contains(spec.getMissionId())) { 124 BaseHubMissionCreator curr = new BaseHubMissionCreator(spec); 125 curr.setWasAutoAdded(true); 126 curr.setActive(true); 127 creators.add(curr); 128 } 129 } 130 } 131 132 protected Object readResolve() { 133 if (recentlyAcceptedTimeout == null) { 134 recentlyAcceptedTimeout = new TimeoutTracker<String>(); 135 } 136 //updateMissionCreatorsFromSpecs(); 137 return this; 138 } 139 140 141 public boolean callEvent(String ruleId, InteractionDialogAPI dialog, 142 List<Token> params, Map<String, MemoryAPI> memoryMap) { 143 String action = params.get(0).getString(memoryMap); 144 if (action.equals("setMHOptionText")) { 145 person.getMemoryWithoutUpdate().set("$mh_openOptionText", getOpenOptionText(), 0); 146 } else if (action.equals("prepare")) { 147 prepare(dialog, memoryMap); 148 } else if (action.equals("listMissions")) { 149 listMissions(dialog, memoryMap, true); 150 } else if (action.equals("returnToList")) { 151 listMissions(dialog, memoryMap, false); 152 } else if (action.equals("doCleanup")) { 153 doCleanup(dialog, memoryMap); 154 } else if (action.equals("accept")) { 155 String missionId = params.get(1).getString(memoryMap); 156 accept(dialog, memoryMap, missionId); 157 } else { 158 throw new RuntimeException("Unhandled action [" + action + "] in " + getClass().getSimpleName() + 159 " for rule [" + ruleId + "], params:[" + params + "]"); 160 } 161 return true; 162 } 163 164 165 public void accept(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap, String missionId) { 166 for (HubMission curr : getOfferedMissions()) { 167 if (curr.getMissionId().equals(missionId)) { 168 curr.accept(dialog, memoryMap); 169 getOfferedMissions().remove(curr); 170 171 float dur = curr.getCreator().getAcceptedTimeoutDuration(); 172 timeout.add(curr.getCreator().getSpecId(), dur); 173 recentlyAcceptedTimeout.add(curr.getCreator().getSpecId(), getUpdateInterval()); 174 break; 175 } 176 } 177 } 178 179 public void prepare(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap) { 180 setDaysSinceLastOpened(getPerson()); 181 updateOfferedMissions(dialog, memoryMap); 182 //offered.clear(); 183 updateCountAndFirstInlineBlurb(dialog, memoryMap); 184 } 185 186 public void doCleanup(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap) { 187 //PersonAPI person = dialog.getInteractionTarget().getActivePerson(); 188 for (HubMission curr : getOfferedMissions()) { 189 curr.abort(); 190 } 191 offered = new ArrayList<HubMission>(); 192 } 193 194 protected void updateCountAndFirstInlineBlurb(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap) { 195 MemoryAPI pMem = dialog.getInteractionTarget().getActivePerson().getMemoryWithoutUpdate(); 196 197 int count = 0; 198 String firstInlineBlurb = null; 199 for (HubMission curr : getOfferedMissions()) { 200 count++; 201 if (firstInlineBlurb == null) firstInlineBlurb = curr.getBlurbText(); 202 } 203 204 pMem.set("$mh_firstInlineBlurb", firstInlineBlurb, 0); 205 pMem.set("$mh_count", count, 0); 206 } 207 208 public void listMissions(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap, boolean withBlurbs) { 209 if (dialog != null && dialog.getVisualPanel() != null) { 210 dialog.getVisualPanel().removeMapMarkerFromPersonInfo(); 211 } 212 MemoryAPI pMem = dialog.getInteractionTarget().getActivePerson().getMemoryWithoutUpdate(); 213 updateCountAndFirstInlineBlurb(dialog, memoryMap); 214 215 if (pMem.getFloat("$mh_count") <= 0) { 216 FireAll.fire(null, dialog, memoryMap, "PopulateOptions"); 217 return; 218 } 219 220 dialog.getOptionPanel().clearOptions(); 221 222 String blurb = "\""; 223 boolean hasCommonBlurb = false; 224 int count = 0; 225 int skipped = 0; 226 for (HubMission curr : getOfferedMissions()) { 227 if (curr.getBlurbText() != null) { 228 blurb += curr.getBlurbText() + " "; 229 FireBest.fire(null, dialog, memoryMap, curr.getTriggerPrefix() + "_option true"); 230 hasCommonBlurb = true; 231 } else { 232 skipped++; 233 } 234 count++; 235 //if (count >= MAX_TO_SHOW) break; 236 } 237 238 count -= skipped; 239 240 // so that the blurbs and the options are in the same order 241 for (HubMission curr : getOfferedMissions()) { 242 //if (count >= MAX_TO_SHOW) break; 243 count++; 244 245 if (curr.getBlurbText() == null) { 246 if (withBlurbs) { 247 if (!FireBest.fire(null, dialog, memoryMap, curr.getTriggerPrefix() + "_blurb true")) { 248 dialog.getTextPanel().addPara("No blurb found for " + curr.getTriggerPrefix(), Misc.getNegativeHighlightColor()); 249 } 250 } 251 if (!FireBest.fire(null, dialog, memoryMap, curr.getTriggerPrefix() + "_option true")) { 252 dialog.getTextPanel().addPara("No option found for " + curr.getTriggerPrefix(), Misc.getNegativeHighlightColor()); 253 } 254 } 255 } 256 257 FireBest.fire(null, dialog, memoryMap, "AddMHCloseOption true"); 258 259 if (withBlurbs && hasCommonBlurb && !pMem.getBoolean("$mh_doNotPrintBlurbs")) { 260 blurb = blurb.trim(); 261 blurb += "\""; 262 dialog.getTextPanel().addPara(blurb); 263 } 264 265 if (withBlurbs) { 266 FireBest.fire(null, dialog, memoryMap, "MHPostMissionListText"); 267 } 268 269 if (Global.getSettings().isDevMode()) { 270 DevMenuOptions.addOptions(dialog); 271 } 272 } 273 274 public String getOpenOptionText() { 275 //Inquire about available jobs 276 return "\"Do you have any work for me?\""; 277 } 278 279 280 protected float getUpdateInterval() { 281 return UPDATE_INTERVAL; 282 } 283 284 public List<HubMission> getOfferedMissions() { 285 return offered; 286 } 287 288 289 protected transient Random missionGenRandom = new Random(); 290 protected long seed = 0; 291 protected long lastUpdated = Long.MIN_VALUE; 292 protected long lastUpdatedSeeds = 0; 293 protected float daysSinceLastUpdate = 0f; 294 public void updateOfferedMissions(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap) { 295 updateMissionCreatorsFromSpecs(); 296 297 float daysElapsed = Global.getSector().getClock().getElapsedDaysSince(lastUpdated); 298 if (lastUpdated <= Long.MIN_VALUE) daysElapsed = getUpdateInterval(); 299 daysSinceLastUpdate += daysElapsed; 300 lastUpdated = Global.getSector().getClock().getTimestamp(); 301 302 timeout.advance(daysElapsed); 303 304 if (daysSinceLastUpdate > getUpdateInterval() || seed == 0) { 305 daysSinceLastUpdate = 0; 306 //seed = Misc.genRandomSeed(); 307 seed = BarEventManager.getInstance().getSeed(null, person, "" + lastUpdatedSeeds); 308 309 recentlyAcceptedTimeout.clear(); 310 for (HubMissionCreator creator : creators) { 311 //creator.updateSeed(); 312 creator.setSeed(BarEventManager.getInstance().getSeed(null, person, 313 creator.getSpecId() + "" + lastUpdatedSeeds)); 314 } 315 lastUpdatedSeeds = Global.getSector().getClock().getTimestamp(); 316 } 317 318 missionGenRandom = new Random(seed); 319 320 //missionGenRandom = Misc.random; 321 322 323 WeightedRandomPicker<HubMissionCreator> picker = new WeightedRandomPicker<HubMissionCreator>(missionGenRandom); 324 WeightedRandomPicker<HubMissionCreator> priority = new WeightedRandomPicker<HubMissionCreator>(missionGenRandom); 325 float rel = person.getRelToPlayer().getRel(); 326 327 Set<String> completed = new LinkedHashSet<String>(); 328 for (HubMissionCreator creator : creators) { 329 if (creator.getNumCompleted() > 0) completed.add(creator.getSpecId()); 330 } 331 332 for (HubMissionCreator creator : creators) { 333// if (creator.getSpecId().equals("mcb")) { 334// System.out.println("fweefwew"); 335// } 336 // keep timeout missions so that after the player accepts a mission 337 // the re-generated set using missionGenRandom remains the same (minus the accepted mission) 338 if (timeout.contains(creator.getSpecId()) && 339 !recentlyAcceptedTimeout.contains(creator.getSpecId())) continue; 340 341 if (!creator.isActive()) continue; 342 343 if (creator.getSpec().hasTag(Tags.MISSION_NON_REPEATABLE) && 344 creator.getNumCompleted() > 0) { 345 continue; 346 } 347 348 if (!DebugFlags.ALLOW_ALL_CONTACT_MISSIONS && !creator.getSpec().completedMissionsMatch(completed)) { 349 continue; 350 } 351 352 353 if (!DebugFlags.ALLOW_ALL_CONTACT_MISSIONS) { 354 if (!creator.matchesRep(rel)) continue; 355 if (!DebugFlags.ALLOW_ALL_CONTACT_MISSIONS) { 356 if (person.getImportance().ordinal() < creator.getSpec().getImportance().ordinal()) continue; 357 } 358 } 359 360 float w = creator.getFrequencyWeight(); 361 if (creator.isPriority()) { 362 priority.add(creator, w); 363 } else { 364 picker.add(creator, w); 365 } 366 } 367 368 369 int bonusMissions = 0; 370 if (person.getMemoryWithoutUpdate().contains(NUM_BONUS_MISSIONS)) { 371 float bonus = person.getMemoryWithoutUpdate().getFloat(NUM_BONUS_MISSIONS); 372 float rem = bonus - (int) bonus; 373 bonusMissions = (int) bonus; 374 if (missionGenRandom.nextFloat() < rem) { 375 bonusMissions++; 376 } 377 } 378 379 int num = MIN_TO_SHOW + missionGenRandom.nextInt(MAX_TO_SHOW - MIN_TO_SHOW + 1) + bonusMissions; 380 if (num > MAX_TO_SHOW_WITH_BONUS) num = MAX_TO_SHOW_WITH_BONUS; 381 if (num < 1 && MIN_TO_SHOW > 0) num = 1; 382 if (DebugFlags.BAR_DEBUG) num = 8; 383 //num = 5; 384 385 if (person.getMemoryWithoutUpdate().getBoolean(CONTACT_SUSPENDED)) { 386 num = 0; 387 } 388 389 offered = new ArrayList<HubMission>(); 390 391// resetMissionAngle(person, person.getMarket()); 392// getMissionAngle(person, person.getMarket(), missionGenRandom); 393 394 ImportantPeopleAPI ip = Global.getSector().getImportantPeople(); 395 ip.resetExcludeFromGetPerson(); 396 // existing contacts don't get picked as targets for missions 397 for (IntelInfoPlugin intel : Global.getSector().getIntelManager().getIntel(ContactIntel.class)) { 398 ip.excludeFromGetPerson(((ContactIntel)intel).getPerson()); 399 } 400 401 while ((!picker.isEmpty() || !priority.isEmpty()) && offered.size() < num) { 402 HubMissionCreator creator = priority.pickAndRemove(); 403 if (creator == null) { 404 creator = picker.pickAndRemove(); 405 } 406 407 // so that if a player accepted a mission, overall set will be the same, minus the accepted missions 408 if (recentlyAcceptedTimeout.contains(creator.getSpecId())) { 409 num--; 410 continue; 411 } 412 413 creator.updateRandom(); 414 HubMission mission = creator.createHubMission(this); 415 if (mission != null) { 416 mission.setHub(this); 417 mission.setCreator(creator); 418 mission.setGenRandom(creator.getGenRandom()); 419 //mission.setGenRandom(Misc.random); 420 mission.createAndAbortIfFailed(getPerson().getMarket(), false); 421 //mission.setGenRandom(null); 422 } 423 if (mission == null || mission.isMissionCreationAborted()) continue; 424 offered.add(mission); 425 mission.updateInteractionData(dialog, memoryMap); 426 427 float dur = creator.getWasShownTimeoutDuration(); 428 timeout.add(creator.getSpecId(), dur); 429 430 //getCreatedMissionsList(person, person.getMarket()).add((BaseHubMission) mission); 431 } 432 433 ip.resetExcludeFromGetPerson(); 434 435 //clearCreatedMissionsList(person, person.getMarket()); 436 } 437 438 public PersonAPI getPerson() { 439 return person; 440 } 441 public void setPerson(PersonAPI person) { 442 this.person = person; 443 } 444 445 446 public static List<PersonMissionSpec> getMissionsForPerson(PersonAPI person) { 447 List<PersonMissionSpec> result = new ArrayList<PersonMissionSpec>(); 448 449 Set<String> personTags = new HashSet<String>(person.getTags()); 450 personTags.add(person.getFaction().getId()); 451 452 for (PersonMissionSpec spec : Global.getSettings().getAllMissionSpecs()) { 453 if (spec.getPersonId() != null && !spec.getPersonId().equals(person.getId())) continue; 454 if (!spec.tagsMatch(personTags)) continue; 455 456 if (spec.getPersonId() == null && spec.getTagsAll().isEmpty() && 457 spec.getTagsAny().isEmpty() && spec.getTagsNotAny().isEmpty()) continue; 458 459 result.add(spec); 460 } 461 return result; 462 } 463 464 465 public static String MISSION_ANGLE_KEY = "$core_missionAngle"; 466 467// public static void resetMissionAngle(PersonAPI person, MarketAPI market) { 468// MemoryAPI mem; 469// if (market != null) { 470// mem = market.getMemoryWithoutUpdate(); 471// } else if (person!= null) { 472// mem = person.getMemoryWithoutUpdate(); 473// } else { 474// return; 475// } 476// mem.unset(MISSION_ANGLE_KEY); 477// } 478 479 //public static float getMissionAngle(PersonAPI person, MarketAPI market, Random random) { 480 public static float getMissionAngle(PersonAPI person, MarketAPI market) { 481 MemoryAPI mem; 482 if (market != null) { 483 mem = market.getMemoryWithoutUpdate(); 484 } else if (person!= null) { 485 mem = person.getMemoryWithoutUpdate(); 486 } else { 487 Random random = Misc.getRandom(BarEventManager.getInstance().getSeed(null, null, null), 11); 488 return random.nextFloat() * 360f; 489 } 490 491 float angle; 492 if (mem.contains(MISSION_ANGLE_KEY)) { 493 angle = mem.getFloat(MISSION_ANGLE_KEY); 494 } else { 495 StarSystemUnexploredReq unexplored = new StarSystemUnexploredReq(); 496 Vector2f loc = Global.getSector().getPlayerFleet().getLocationInHyperspace(); 497 498 SectorEntityToken entity = null; 499 if (market != null) entity = market.getPrimaryEntity(); 500 if (entity == null && person != null && person.getMarket() != null) { 501 entity = person.getMarket().getPrimaryEntity(); 502 } 503 Random random = Misc.getRandom(BarEventManager.getInstance().getSeed(entity, person, null), 11); 504 WeightedRandomPicker<Float> picker = new WeightedRandomPicker<Float>(random); 505 for (StarSystemAPI system : Global.getSector().getStarSystems()) { 506 float dir = Misc.getAngleInDegrees(loc, system.getLocation()); 507 if (unexplored.systemMatchesRequirement(system)) { 508 picker.add(dir, 1f); 509 } else { 510 float days = system.getDaysSinceLastPlayerVisit(); 511 float weight = days / 1000f; 512 if (weight < 0.01f) weight = 0.01f; 513 if (weight > 1f) weight = 1f; 514 picker.add(dir, weight * 0.01f); 515 } 516 } 517 518 angle = picker.pick(); 519 520 mem.set(MISSION_ANGLE_KEY, angle, 0f); 521 } 522 return angle; 523 } 524 525 526// public static String CREATED_MISSIONS_KEY = "$core_createdMissions"; 527// @SuppressWarnings("unchecked") 528// public static List<BaseHubMission> getCreatedMissionsList(PersonAPI person, MarketAPI market) { 529// MemoryAPI mem; 530// if (market != null) { 531// mem = market.getMemoryWithoutUpdate(); 532// } else if (person!= null) { 533// mem = person.getMemoryWithoutUpdate(); 534// } else { 535// return new ArrayList<BaseHubMission>(); 536// } 537// List<BaseHubMission> list = (List<BaseHubMission>) mem.get(CREATED_MISSIONS_KEY); 538// if (list == null) { 539// list = new ArrayList<BaseHubMission>(); 540// mem.set(CREATED_MISSIONS_KEY, list); 541// } 542// return list; 543// } 544// 545// public static void clearCreatedMissionsList(PersonAPI person, MarketAPI market) { 546// MemoryAPI mem; 547// if (market != null) { 548// mem = market.getMemoryWithoutUpdate(); 549// } else if (person!= null) { 550// mem = person.getMemoryWithoutUpdate(); 551// } else { 552// return; 553// } 554// mem.unset(CREATED_MISSIONS_KEY); 555// } 556} 557 558 559 560 561 562 563 564 565