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