forked from kolmafia/kolmafia
-
Notifications
You must be signed in to change notification settings - Fork 0
/
AdventureRequest.java
1233 lines (1070 loc) 路 43.9 KB
/
AdventureRequest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package net.sourceforge.kolmafia.request;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.KoLAdventure;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLConstants.MafiaState;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.MonsterData;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.combat.CombatActionManager;
import net.sourceforge.kolmafia.combat.MonsterStatusTracker;
import net.sourceforge.kolmafia.objectpool.AdventurePool;
import net.sourceforge.kolmafia.objectpool.EffectPool;
import net.sourceforge.kolmafia.objectpool.FamiliarPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.AdventureDatabase;
import net.sourceforge.kolmafia.persistence.AdventureQueueDatabase;
import net.sourceforge.kolmafia.persistence.AdventureSpentDatabase;
import net.sourceforge.kolmafia.persistence.MonsterDatabase;
import net.sourceforge.kolmafia.persistence.QuestDatabase;
import net.sourceforge.kolmafia.persistence.QuestDatabase.Quest;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.session.BatManager;
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.ConsequenceManager;
import net.sourceforge.kolmafia.session.CrystalBallManager;
import net.sourceforge.kolmafia.session.DvorakManager;
import net.sourceforge.kolmafia.session.EncounterManager;
import net.sourceforge.kolmafia.session.EquipmentManager;
import net.sourceforge.kolmafia.session.GoalManager;
import net.sourceforge.kolmafia.session.Limitmode;
import net.sourceforge.kolmafia.session.LouvreManager;
import net.sourceforge.kolmafia.session.ResultProcessor;
import net.sourceforge.kolmafia.session.SorceressLairManager;
import net.sourceforge.kolmafia.session.TavernManager;
import net.sourceforge.kolmafia.session.TurnCounter;
import net.sourceforge.kolmafia.session.WumpusManager;
import net.sourceforge.kolmafia.swingui.RequestSynchFrame;
import net.sourceforge.kolmafia.utilities.HTMLParserUtils;
import net.sourceforge.kolmafia.utilities.StringUtilities;
import net.sourceforge.kolmafia.webui.BarrelDecorator;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.XPatherException;
public class AdventureRequest extends GenericRequest {
public static final String NOT_IN_A_FIGHT = "Not in a Fight";
private static final Pattern AREA_PATTERN =
Pattern.compile("(adv|snarfblat)=(\\d*)", Pattern.DOTALL);
// <img id='monpic' src="http://images.kingdomofloathing.com/adventureimages/ssd_sundae.gif"
// width=100 height=100>
private static final Pattern MONSTER_IMAGE =
Pattern.compile("<img +id='monpic'.*?adventureimages/(.*?\\.gif)");
private static final GenericRequest ZONE_UNLOCK = new GenericRequest("");
private final String adventureName;
private final String formSource;
private final String adventureId;
private int override = -1;
/**
* Constructs a new <code>AdventureRequest</code> which executes the adventure designated by the
* given Id by posting to the provided form, notifying the givenof results (or errors).
*
* @param adventureName The name of the adventure location
* @param formSource The form to which the data will be posted
* @param adventureId The identifier for the adventure to be executed
*/
public AdventureRequest(
final String adventureName, final String formSource, final String adventureId) {
super(formSource);
this.adventureName = adventureName;
this.formSource = formSource;
this.adventureId = adventureId;
// The adventure Id is all you need to identify the adventure;
// posting it in the form sent to adventure.php will handle
// everything for you.
// Those that change mid session should be added to run() also.
if (formSource.equals("adventure.php")) {
this.addFormField("snarfblat", adventureId);
} else if (formSource.equals("casino.php")) {
this.addFormField("action", "slot");
this.addFormField("whichslot", adventureId);
} else if (formSource.equals("crimbo10.php")) {
this.addFormField("place", adventureId);
} else if (formSource.equals("cobbsknob.php")) {
this.addFormField("action", "throneroom");
} else if (formSource.equals("friars.php")) {
this.addFormField("action", "ritual");
} else if (formSource.equals("invasion.php")) {
this.addFormField("action", adventureId);
} else if (formSource.equals("mining.php")) {
this.addFormField("mine", adventureId);
} else if (formSource.equals("place.php")) {
if (adventureId.equals("cloudypeak2")) {
this.addFormField("whichplace", "mclargehuge");
this.addFormField("action", adventureId);
} else if (this.adventureId.equals("pyramid_state")) {
this.addFormField("whichplace", "pyramid");
StringBuilder action = new StringBuilder();
action.append(adventureId);
action.append(Preferences.getString("pyramidPosition"));
if (Preferences.getBoolean("pyramidBombUsed")) {
action.append("a");
}
this.addFormField("action", action.toString());
} else if (this.adventureId.equals("manor4_chamberboss")) {
this.addFormField("whichplace", "manor4");
this.addFormField("action", adventureId);
} else if (this.adventureId.equals("townwrong_tunnel")) {
this.addFormField("whichplace", "town_wrong");
this.addFormField("action", "townwrong_tunnel");
} else if (this.adventureId.startsWith("ns_")) {
this.addFormField("whichplace", "nstower");
this.addFormField("action", adventureId);
} else if (this.adventureId.equals("town_eincursion")
|| this.adventureId.equals("town_eicfight2")) {
this.addFormField("whichplace", "town");
this.addFormField("action", adventureId);
} else if (this.adventureId.equals("ioty2014_wolf")) {
this.addFormField("whichplace", "manor4");
this.addFormField("action", "wolf_houserun");
}
} else if (!formSource.equals("basement.php")
&& !formSource.equals("cellar.php")
&& !formSource.equals("barrel.php")) {
this.addFormField("action", adventureId);
}
}
@Override
protected boolean retryOnTimeout() {
return true;
}
@Override
public void run() {
// Prevent the request from happening if they attempted
// to cancel in the delay period.
if (!KoLmafia.permitsContinue()) {
return;
} else if (this.formSource.equals("adventure.php")) {
if (this.adventureId.equals(AdventurePool.THE_SHORE_ID)) {
// The Shore
int adv = KoLCharacter.inFistcore() ? 5 : 3;
if (KoLCharacter.getAdventuresLeft() < adv) {
KoLmafia.updateDisplay(MafiaState.ERROR, "Ran out of adventures.");
return;
}
}
} else if (this.formSource.equals("barrel.php")) {
int square = BarrelDecorator.recommendSquare();
if (square == 0) {
KoLmafia.updateDisplay(
MafiaState.ERROR, "All booze in the specified rows has been collected.");
return;
}
this.addFormField("smash", String.valueOf(square));
} else if (this.formSource.equals("cellar.php")) {
if (TavernManager.shouldAutoFaucet()) {
this.removeFormField("whichspot");
this.addFormField("action", "autofaucet");
} else {
int square = TavernManager.recommendSquare();
if (square == 0) {
KoLmafia.updateDisplay(
MafiaState.ERROR, "Don't know which square to visit in the Typical Tavern Cellar.");
return;
}
this.addFormField("whichspot", String.valueOf(square));
this.addFormField("action", "explore");
}
} else if (this.formSource.equals("mining.php")) {
KoLmafia.updateDisplay(MafiaState.ERROR, "Automated mining is not currently implemented.");
return;
} else if (this.formSource.equals("place.php") && this.adventureId.equals("pyramid_state")) {
this.addFormField("whichplace", "pyramid");
StringBuilder action = new StringBuilder();
action.append(adventureId);
action.append(Preferences.getString("pyramidPosition"));
if (Preferences.getBoolean("pyramidBombUsed")) {
action.append("a");
}
this.addFormField("action", action.toString());
} else if (this.formSource.equals("place.php") && this.adventureId.equals("manor4_chamber")) {
this.addFormField("whichplace", "manor4");
if (!QuestDatabase.isQuestFinished(Quest.MANOR)) {
this.addFormField("action", "manor4_chamberboss");
} else {
this.addFormField("action", "manor4_chamber");
}
}
super.run();
}
@Override
public void processResults() {
// Sometimes, there's no response from the server.
// In this case, skip and continue onto the next one.
if (this.responseText == null
|| this.responseText.trim().length() == 0
|| this.responseText.contains("No, that isn't a place yet.")) {
KoLmafia.updateDisplay(MafiaState.ERROR, "You can't get to that area yet.");
return;
}
if (this.formSource.equals("place.php")) {
if (this.getURLString().contains("whichplace=nstower")) {
// nstower locations redirect to a fight or choice. If
// it didn't do that, you can't adventure there.
KoLmafia.updateDisplay(MafiaState.PENDING, "You can't adventure there.");
SorceressLairManager.parseTowerResponse("", this.responseText);
return;
}
}
// We're missing an item, haven't been given a quest yet, or
// otherwise trying to go somewhere not allowed.
int index = KoLAdventure.findAdventureFailure(this.responseText);
if (index >= 0) {
String failure = KoLAdventure.adventureFailureMessage(index);
MafiaState severity = KoLAdventure.adventureFailureSeverity(index);
KoLmafia.updateDisplay(severity, failure);
this.override = 0;
return;
}
// This is a server error. Hope for the best and repeat the
// request.
if (this.responseText.contains("No adventure data exists for this location")) {
KoLmafia.updateDisplay(MafiaState.ERROR, "Server error. Please wait and try again.");
return;
}
// Nothing more to do in this area
if (this.formSource.equals("adventure.php")) {
if (this.adventureId.equals(AdventurePool.MERKIN_COLOSSEUM_ID)) {
SeaMerkinRequest.parseColosseumResponse(this.getURLString(), this.responseText);
}
if (!this.responseText.contains("adventure.php")
&& !this.responseText.contains("You acquire")) {
if (!EncounterManager.isAutoStop(this.encounter)) {
KoLmafia.updateDisplay(MafiaState.PENDING, "Nothing more to do here.");
}
return;
}
}
// If you're at the casino, each of the different slot
// machines deducts meat from your tally
if (this.formSource.equals("casino.php")) {
if (this.adventureId.equals("1")) {
ResultProcessor.processMeat(-5);
} else if (this.adventureId.equals("2")) {
ResultProcessor.processMeat(-10);
} else if (this.adventureId.equals("11")) {
ResultProcessor.processMeat(-10);
}
}
if (this.adventureId.equals(AdventurePool.ROULETTE_TABLES_ID)) {
ResultProcessor.processMeat(-10);
} else if (this.adventureId.equals(String.valueOf(AdventurePool.POKER_ROOM))) {
ResultProcessor.processMeat(-30);
}
// Trick-or-treating requires a costume;
// notify the user of this error.
if (this.formSource.equals("trickortreat.php")
&& this.responseText.contains("without a costume")) {
KoLmafia.updateDisplay(MafiaState.ERROR, "You must wear a costume.");
return;
}
}
public static final String registerEncounter(final GenericRequest request) {
// No encounters in chat!
if (request.isChatRequest) {
return "";
}
String urlString = request.getURLString();
String responseText = request.responseText;
boolean isFight = urlString.startsWith("fight.php") || urlString.startsWith("fambattle.php");
boolean isChoice = urlString.startsWith("choice.php");
// If we were redirected into a fight or a choice through using
// an item, there will be an encounter in the responseText.
// Otherwise, if KoLAdventure didn't log the location, there
// can't be an encounter for us to log.
if (GenericRequest.itemMonster == null
&& !KoLAdventure.recordToSession(urlString, responseText)) {
return "";
}
if (!(request instanceof AdventureRequest)
&& !AdventureRequest.containsEncounter(urlString, responseText)) {
return "";
}
String encounter = null;
String type = null;
if (isFight) {
type = "Combat";
encounter = AdventureRequest.parseCombatEncounter(responseText);
} else if (isChoice) {
int choice = ChoiceManager.extractChoice(responseText);
type = choiceType(choice);
encounter = AdventureRequest.parseChoiceEncounter(urlString, choice, responseText);
ChoiceManager.registerDeferredChoice(choice, encounter);
} else {
type = "Noncombat";
encounter = parseNoncombatEncounter(urlString, responseText);
if (responseText.contains("charpane.php")) {
// Since a charpane refresh was requested, this might have taken a turn
AdventureSpentDatabase.setNoncombatEncountered(true);
}
}
if (encounter == null) {
return "";
}
// Silly check for silly situation
if (encounter == AdventureRequest.NOT_IN_A_FIGHT) {
return encounter;
}
if (isFight) {
FightRequest.setCurrentEncounter(encounter);
}
if (KoLCharacter.inDisguise()) {
if (encounter.equals("The Bonerdagon")) {
encounter = "Boss Bat wearing a Bonerdagon mask";
} else if (encounter.equals("The Naughty Sorceress")) {
encounter = "Knob Goblin King wearing a Naughty Sorceress mask";
} else if (encounter.equals("Groar")) {
encounter = "Bonerdagon wearing a Groar mask";
} else if (encounter.equals("Ed the Undying")) {
encounter = "Groar wearing an Ed the Undying mask";
} else if (encounter.equals("The Big Wisniewski")) {
encounter = "The Man wearing a Big Wisniewski mask";
} else if (encounter.equals("The Man")) {
encounter = "The Big Wisniewski wearing a The Man mask";
} else if (encounter.equals("The Boss Bat")) {
encounter = "Naughty Sorceress wearing a Boss Bat mask";
}
}
Preferences.setString("lastEncounter", encounter);
RequestLogger.printLine("Encounter: " + encounter);
RequestLogger.updateSessionLog("Encounter: " + encounter);
AdventureRequest.registerDemonName(encounter, responseText);
// We are done registering the item's encounter.
if (type != null) {
if (type.equals("Combat")) {
MonsterData monster = AdventureRequest.extractMonster(encounter, responseText);
// In Ed we'll only set the monster name the first time we encounter the monster
if (!KoLCharacter.isEd() || Preferences.getInteger("_edDefeats") == 0) {
MonsterStatusTracker.setNextMonster(monster);
}
encounter = monster.getName();
// Only queue normal monster encounters
if (!EncounterManager.ignoreSpecialMonsters
&& !EncounterManager.isWanderingMonster(encounter)
&& !EncounterManager.isUltrarareMonster(encounter)
&& !EncounterManager.isSemiRareMonster(encounter)
&& !EncounterManager.isSuperlikelyMonster(encounter)
&& !EncounterManager.isFreeCombatMonster(encounter)
&& !EncounterManager.isNoWanderMonster(encounter)
&& !EncounterManager.isEnamorangEncounter(responseText, false)
&& !EncounterManager.isDigitizedEncounter(responseText, false)
&& !EncounterManager.isRomanticEncounter(responseText, false)
&& !EncounterManager.isSaberForceMonster()
&& !CrystalBallManager.isCrystalBallMonster()
&& !FightRequest.edFightInProgress()) {
AdventureQueueDatabase.enqueue(KoLAdventure.lastVisitedLocation(), encounter);
}
} else if (type.equals("Noncombat")) {
// only log the FIRST choice that we see in a choiceadventure chain.
if ((!urlString.startsWith("choice.php") || ChoiceManager.getLastChoice() == 0)
&& !FightRequest.edFightInProgress()) {
AdventureQueueDatabase.enqueueNoncombat(KoLAdventure.lastVisitedLocation(), encounter);
}
}
EncounterManager.registerEncounter(encounter, type, responseText);
}
TurnCounter.handleTemporaryCounters(type, encounter);
if (!Preferences.getString("crystalBallPredictions").isEmpty()) {
CrystalBallManager.updateCrystalBallPredictions();
}
return encounter;
}
private static String fromName = null;
private static String toName = null;
public static final void setNameOverride(final String from, final String to) {
fromName = from;
toName = to;
}
private static final Pattern[] MONSTER_NAME_PATTERNS = {
Pattern.compile("You're fighting <span id='monname'> *(.*?)</span>", Pattern.DOTALL),
// papier weapons can change "fighting" to some other verb
Pattern.compile("You're (?:<u>.*?</u>) <span id='monname'>(.*?)</span>", Pattern.DOTALL),
// Pocket Familiars have Pokefam battles
// <b><center>a fleet woodsman's Team:</b>
Pattern.compile(">([^<]*?)'s Team:<"),
// KoL sure generates a lot of bogus HTML
Pattern.compile("<b>.*?(<b>.*?<(/b|/td)>.*?)<(br|/td|/tr)>", Pattern.DOTALL),
};
public static final String parseCombatEncounter(final String responseText) {
// Silly check for silly situation
if (responseText.contains("Not in a Fight")) {
return AdventureRequest.NOT_IN_A_FIGHT;
}
String name = null;
for (Pattern pattern : MONSTER_NAME_PATTERNS) {
Matcher matcher = pattern.matcher(responseText);
if (matcher.find()) {
name = matcher.group(1);
break;
}
}
if (name == null) {
return "";
}
// If the name has bold markup, strip formatting
name = StringUtilities.globalStringReplace(name, "<b>", "");
name = StringUtilities.globalStringReplace(name, "</b>", "");
// Brute force fix for haiku dungeon monsters, which have
// punctuation at the end because of bad HTML
name =
name.startsWith("amateur ninja")
? "amateur ninja"
: name.startsWith("ancient insane monk")
? "ancient insane monk"
: name.startsWith("Ferocious bugbear")
? "ferocious bugbear"
: name.startsWith("gelatinous cube")
? "gelatinous cube"
: name.startsWith("Knob Goblin poseur") ? "Knob Goblin poseur" : name;
// Canonicalize
name = CombatActionManager.encounterKey(name, false);
// Coerce name if needed
if (name.equalsIgnoreCase(fromName)) {
name = CombatActionManager.encounterKey(toName, false);
}
fromName = null;
EquipmentManager.decrementTurns();
return name;
}
// <!-- MONSTERID: 112 -->
private static final Pattern MONSTERID_PATTERN = Pattern.compile("<!-- MONSTERID: (\\d+) -->");
private static final MonsterData WUMPUS = MonsterDatabase.findMonster("wumpus");
private static final MonsterData THE_DARKNESS =
MonsterDatabase.findMonster("the darkness (blind)");
public static final MonsterData extractMonster(
final String encounterToCheck, final String responseText) {
// We need to extract the random modifiers and masks, since
// those will be added to the MonsterData
String encounter =
AdventureRequest.handleRandomModifiers(encounterToCheck.trim(), responseText);
encounter = AdventureRequest.handleIntergnat(encounter);
encounter = AdventureRequest.handleNuclearAutumn(encounter);
encounter = AdventureRequest.handleMask(encounter);
// KoL now provides MONSTERID in fight responseText.
Matcher m = MONSTERID_PATTERN.matcher(responseText);
if (!m.find()) {
// It does not do this if and only if you are blind.
if (responseText.contains("darkness.gif")) {
// Adventuring in the Wumpus cave while temporarily blind is
// foolish, but since we won't clear the cave after defeating
// it if we can't recognize it, allow for it
return WumpusManager.isWumpus() ? WUMPUS : THE_DARKNESS;
}
// As of 16-June-2020, KoL will provide MONSTERID with
// every round of combat. If it fails to do so when you
// are not blind, That would be a bug. Log it and
// attempt to identify the monster by name.
StaticEntity.printDebugText("MONSTERID not found", responseText);
encounter = ConsequenceManager.disambiguateMonster(encounter, responseText);
MonsterData monster = MonsterDatabase.findMonster(encounter);
return (monster != null) ? monster : MonsterDatabase.registerMonster(encounter);
}
int monsterId = StringUtilities.parseInt(m.group(1));
MonsterData monster = MonsterDatabase.findMonsterById(monsterId);
// Do we know this monster id?
if (monster != null) {
// Yes. Send through ConsequenceManager. This is how we
// disambiguate Ed the Undying into Ed the Undying (4),
// for example.
String monsterName = monster.getName();
String disambiguated = ConsequenceManager.disambiguateMonster(monsterName, responseText);
if (!monsterName.equals(disambiguated)) {
return MonsterDatabase.findMonster(disambiguated);
}
return monster;
}
// No! Is this a monster for which we have a pseudo-ID?
encounter = ConsequenceManager.disambiguateMonster(encounter, responseText);
monster = MonsterDatabase.findMonster(encounter);
String image = null;
if (monster != null) {
// Yes. We've learned the actual monsterId!
MonsterDatabase.setMonsterId(monster, monsterId);
image = monster.getImage();
} else {
// It's a brand-new monster. Register it.
Matcher i = AdventureRequest.MONSTER_IMAGE.matcher(responseText);
image = i.find() ? i.group(1) : "";
monster = MonsterDatabase.registerMonster(encounter, monsterId, image);
}
String message =
"*** Monster '"
+ encounter
+ "' has monsterId = "
+ monsterId
+ " and image '"
+ image
+ "'";
RequestLogger.printLine(message);
RequestLogger.updateSessionLog(message);
return monster;
}
private static String parseChoiceEncounter(
final String urlString, final int choice, final String responseText) {
if (LouvreManager.louvreChoice(choice)) {
return LouvreManager.encounterName(choice);
}
int urlChoice = ChoiceManager.extractChoiceFromURL(urlString);
int urlOption = ChoiceManager.extractOptionFromURL(urlString);
switch (urlChoice) {
case 1334: // Boxing Daycare (Lobby)
if (urlOption == 1) {
// Have a Boxing Daydream
return "Have a Boxing Daydream";
}
return null;
case 1335: // Boxing Day Spa
if (urlOption >= 1 && urlOption <= 4) {
// (Get a buff)
return "Visit the Boxing Day Spa";
}
return null;
case 1336: // Boxing Daycare
if (urlOption >= 1 && urlOption <= 4) {
// (recruit, scavenge, hire, spar)
return "Enter the Boxing Daycare";
}
return null;
}
switch (choice) {
case 443: // Chess Puzzle
// No "encounter" when moving on the chessboard
if (urlString.contains("xy")) {
return null;
}
break;
case 1085: // Deck of Every Card
return DeckOfEveryCardRequest.parseCardEncounter(responseText);
case 535: // Deep Inside Ronald, Baby
case 536: // Deep Inside Grimace, Bow Chick-a Bow Bow
case 585: // Screwing Around!
case 595: // Fire! I... have made... fire!
case 807: // Breaker Breaker!
case 1003: // Test Your Might And Also Test Other Things
case 1086: // Pick a Card
return null;
case 1135: // The Bat-Sedan
return BatManager.parseBatSedan(responseText);
case 1388:
{
if (!BeachCombRequest.containsEncounter(urlString)) {
return null;
}
break;
}
}
// No "encounter" for certain arcade games
if (ArcadeRequest.arcadeChoice(choice)) {
return null;
}
if (ChoiceManager.canWalkFromChoice(choice)) {
return null;
}
return AdventureRequest.parseEncounter(responseText);
}
private static String choiceType(final int choice) {
if (LouvreManager.louvreChoice(choice)) {
return null;
}
return "Noncombat";
}
private static final String[][] LIMERICKS = {
{"Nantucket Snapper", "ancient old turtle"},
{"The Apathetic Lizardman", "lizardman quite apathetic"},
{"The Bleary-Eyed Cyclops", "bleary-eyed cyclops"},
{"The Crass Goblin", "goblin is crass"},
{"The Crotchety Wizard", "crotchety wizard"},
{"The Dumb Minotaur", "dumb minotaur"},
{"The Fierce-Looking Giant", "fierce-looking giant"},
{"The Gelatinous Cube", "gelatinous cube"},
{"The Gnome with a Truncheon", "gnome with a truncheon"},
{"The Goblin King's Vassal", "Goblin King's vassal"},
{"The Insatiable Maiden", "insatiable maiden"},
{"The Jewelry Gnoll", "bejeweled it"},
{"The Martini Booth", "martini booth"},
{"The One-Legged Trouser", "one-pantlegged schnauzer"},
{"The Orc With a Spork", "waving a spork"},
{"The Slime Puddle", "slime puddle"},
{"The Sozzled Old Dragon", "sozzled old dragon"},
{"The Superior Ogre", "I am superior"},
{"The Unguarded Chest", "chest full of meat"},
{"The Unpleasant Satyr", "unpleasant satyr"},
{"The Vampire Buffer", "vampire buffer"},
{"The Weathered Old Chap", "weathered old chap"},
{"The Witch", "A witch"},
{"Thud", "hobo glyphs"},
};
private static String parseNoncombatEncounter(final String urlString, final String responseText) {
// Fernswarthy's Basement
if (urlString.startsWith("basement.php")) {
return null;
}
if (urlString.startsWith("adventure.php")) {
int area = parseArea(urlString);
switch (area) {
case 17:
// Hidden Temple
// Dvorak's revenge
// You jump to the last letter, and put your pom-poms down with a sign of relief --
// thank goodness that's over. Worst. Spelling bee. Ever.
if (responseText.contains("put your pom-poms down")) {
QuestDatabase.setQuestProgress(Quest.WORSHIP, "step2");
}
break;
case 19:
// Limerick Dungeon
for (int i = 0; i < LIMERICKS.length; ++i) {
if (responseText.contains(LIMERICKS[i][1])) {
return LIMERICKS[i][0];
}
}
return "Unrecognized Limerick";
case 114: // Outskirts of The Knob
// Unstubbed
// You go back to the tree where the wounded Knob Goblin guard was resting,
// and find him just where you left him, continuing to whine about his stubbed toe.
//
// "Here you go, tough guy" you say, and hand him the unguent.
if (responseText.contains("you say, and hand him the unguent")) {
ResultProcessor.processItem(ItemPool.PUNGENT_UNGUENT, -1);
}
break;
}
} else if (urlString.startsWith("barrel.php")) {
// Without this special case, encounter names in the Barrels would
// be things like "bottle of rum"
return "Barrel Smash";
}
String encounter = parseEncounter(responseText);
if (encounter != null) {
return encounter;
}
return null;
}
private static String parseEncounter(final String responseText) {
// Look only in HTML body; the header can have scripts with
// bold text.
int index = responseText.indexOf("<body>");
// Skip past the Adventure Results
int brIndex = responseText.indexOf("Results:</b>", index);
if (brIndex != -1) {
int resultsIndex = responseText.indexOf("<div id=\"results\">", index);
if (resultsIndex != -1) {
// KoL was nice enough to put results into a div for us
index = responseText.indexOf("</div>", resultsIndex);
} else {
// There is no results div, but it doesn't mean that
// there aren't results. Nothing like consistency. Not.
index = brIndex;
}
}
int boldIndex = responseText.indexOf("<b>", index);
if (boldIndex == -1) {
return "";
}
int endBoldIndex = responseText.indexOf("</b>", boldIndex);
if (endBoldIndex == -1) {
return "";
}
return responseText.substring(boldIndex + 3, endBoldIndex);
}
private static int parseArea(final String urlString) {
Matcher matcher = AREA_PATTERN.matcher(urlString);
if (matcher.find()) {
return StringUtilities.parseInt(matcher.group(2));
}
return -1;
}
private static final Object[][] demons = {
{
"Summoning Chamber",
Pattern.compile("Did you say your name was (.*?)\\?"),
"delicious-looking pies",
"demonName1",
},
{
"Hoom Hah", Pattern.compile("(.*?)! \\1, cooooome to meeeee!"), "fifty meat", "demonName2",
},
{
"Every Seashell Has a Story to Tell If You're Listening",
Pattern.compile("Hello\\? Is (.*?) there\\?"),
"fish-guy",
"demonName3",
},
{
"Leavesdropping",
Pattern.compile("(.*?), we call you! \\1, come to us!"),
"bullwhip",
"demonName4",
},
{
"These Pipes... Aren't Clean!",
Pattern.compile("Blurgle. (.*?). Gurgle. By the way,"),
"coprodaemon",
"demonName5",
},
{
"Flying In Circles",
// SC: Then his claws slip, and he falls
// backwards.<p>"<Demon Name>!" he screams as he
// tumbles backwards. "LORD OF REVENGE! GIVE ME
// STRENGTH!"
//
// TT: With a scrape, her sickle slips from the
// rock.<p>"<Demon Name>" she shrieks as she plummets
// toward the lava. "Lord of Revenge! I accept your
// contract! Give me your power!"
//
// PA: Its noodles lose their grip, and the evil
// pastaspawn falls toward the churning
// lava.<p><i>"<Demon Name>!"</i> it howls. "<i>Lord of
// Revenge! Come to my aid!</i>"
//
// SA: As it falls, a mouth opens on its surface and
// howls: "<Demon Name>! Revenge!"
//
// DB: His grip slips, and he falls.<p>"<Demon Name>!
// Lord of Revenge! I call to you! I pray to you! Help
// m--"
//
// AT: His grip slips, and he tumbles
// backward.<p>"<Demon Name>!" he screams. "Emperador
// de la Venganza! Come to my aid! I beg of you!"
Pattern.compile(
"(?:he falls backwards|her sickle slips from the rock|falls toward the churning lava|a mouth opens on its surface and howls|His grip slips, and he falls|he tumbles backward).*?(?:<i>)?"(.*?)!?"(?:</i>)?(?: he screams| she shrieks| it howls| Revenge| Lord of Revenge)"),
"Lord of Revenge",
"demonName8",
},
{
"Sinister Ancient Tablet",
Pattern.compile("<font.*?color=#cccccc>(.*?)</font>"),
"flame-wreathed mouth",
"demonName9",
},
{
"Strange Cube",
Pattern.compile("Come to me! Come to (.*?)!"),
"writhing and twisting snake",
"demonName10",
},
{
"Where Have All The Drunkards Gone?",
Pattern.compile("Is (.*?) a word?"),
"Gary's friend",
"demonName11",
},
};
private static final Pattern NAME_PATTERN = Pattern.compile("<b>"(.*?)"</b>");
public static final boolean registerDemonName(final String encounter, final String responseText) {
String place = null;
String demon = null;
String setting = null;
for (int i = 0; i < AdventureRequest.demons.length; ++i) {
Object[] demons = AdventureRequest.demons[i];
place = (String) demons[0];
if (place == null || !place.equals(encounter)) {
continue;
}
Pattern pattern = (Pattern) demons[1];
Matcher matcher = pattern.matcher(responseText);
if (matcher.find()) {
// We found the name
demon = matcher.group(1);
setting = (String) demons[3];
}
break;
}
// If we didn't recognize the demon and he used a valid name in
// the Summoning Chamber, we can deduce which one it is from
// the result text
if (setting == null && encounter.equals("Summoning Chamber")) {
place = encounter;
Matcher matcher = AdventureRequest.NAME_PATTERN.matcher(responseText);
if (!matcher.find()) {
return false;
}
// Save the name he used.
demon = matcher.group(1);
// Look for tell-tale string
for (int i = 0; i < AdventureRequest.demons.length; ++i) {
Object[] demons = AdventureRequest.demons[i];
String text = (String) demons[2];
if (responseText.contains(text)) {
setting = (String) demons[3];
break;
}
}
}
// Couldn't figure out which demon he called.
if (setting == null) {
return false;
}
String previousName = Preferences.getString(setting);
if (previousName.equals(demon)) {
// Valid demon name
return true;
}
RequestLogger.printLine("Demon name: " + demon);
RequestLogger.updateSessionLog("Demon name: " + demon);
Preferences.setString(setting, demon);
GoalManager.checkAutoStop(place); //
// Valid demon name
return true;
}
private static boolean containsEncounter(final String formSource, final String responseText) {
if (formSource.startsWith("adventure.php")) {
return true;
} else if (formSource.startsWith("fight.php") || formSource.startsWith("fambattle.php")) {
return FightRequest.getCurrentRound() == 0;
} else if (formSource.startsWith("choice.php")) {
return responseText.contains("choice.php");
} else if (formSource.startsWith("cave.php")) {
return formSource.contains("sanctum");
} else if (formSource.startsWith("cobbsknob.php")) {
return formSource.contains("throneroom");
} else if (formSource.startsWith("crypt.php")) {
return formSource.contains("action");
} else if (formSource.startsWith("cellar.php")) {
// Simply visiting the map is not an encounter.
return !formSource.equals("cellar.php");
} else if (formSource.startsWith("suburbandis.php")) {
return formSource.contains("action=dothis");
} else if (formSource.startsWith("tiles.php")) {
// Only register initial encounter of Dvorak's Revenge
DvorakManager.saveResponse(responseText);
return responseText.contains("I before E, except after C");
} else if (formSource.startsWith("barrel.php?smash")) {
return true;
} else if (formSource.startsWith("mining.php")) {
if (formSource.contains("which=")) {
if (!formSource.contains("mine=6")) {
// The Velvet / Gold Mine (mine=6) does not show up as the Last Adventure in
// charpane/api, so we can't track turns_spent there. To avoid counting it
// for the wrong location, don't count it anywhere.
AdventureSpentDatabase.setNoncombatEncountered(true);
}
}
return false;
}
// It is not a known adventure. Therefore,
// do not log the encounter yet.
return false;
}
@Override
public int getAdventuresUsed() {
if (this.override >= 0) {
return this.override;
}
String limitmode = KoLCharacter.getLimitmode();
if (limitmode == Limitmode.SPELUNKY || limitmode == Limitmode.BATMAN) {
return 0;
}
if (this.adventureId.equals(AdventurePool.THE_SHORE_ID)) {
return KoLCharacter.inFistcore() ? 5 : 3;
}
String zone = AdventureDatabase.getZone(this.adventureName);
if (zone != null
&& (zone.equals("The Sea") || this.adventureId.equals(AdventurePool.YACHT_ID))) {
return KoLConstants.activeEffects.contains(EffectPool.get(EffectPool.FISHY)) ? 1 : 2;
}
return 1;
}
public void overrideAdventuresUsed(int used) {
this.override = used;