/
h3m2herowo.php
2941 lines (2549 loc) · 118 KB
/
h3m2herowo.php
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
<?php
use HeroWO\H3M;
$_takeOver = count(get_included_files()) < 2;
require_once __DIR__.'/core.php';
$h3m2json = __DIR__.'/h3m2json.php';
is_file($h3m2json) or $h3m2json = __DIR__.'/h3m2json/h3m2json.php';
if (is_file($h3m2json)) {
require $h3m2json;
class ConvertError extends H3M\CliError {}
} else {
abstract class StubCLI {
function helpText() {
// Stop looking. I mean it!
}
function takeOver() {
echo $this->helpText();
exit(10);
}
}
class_alias(StubCLI::class, 'HeroWO\\H3M\\CLI');
}
class CLI extends H3M\CLI {
// Determined empirically.
static $memory_limit = 1024;
public $scriptFile = 'h3m2herowo.php';
public $databankPath;
public $debugFiles = false;
public $outputHeroWoSubfolder = true; // false, true, 'title'
protected $convertor;
function helpText() {
$ds = DIRECTORY_SEPARATOR;
$text = <<<HELP
Usage: $this->scriptFile [-options] -d databank/ input/|map.ext [output/]
Converts HoMM 3 maps to HeroWO format. Put h3m2json.php into $this->scriptFile's
folder or into h3m2json$ds in that folder (git clone).
Databank (-d) must match the map's game modification, if any (HotA, etc.).
Options specific to $this->scriptFile:
-d PATH mandatory: path to databank folder (produced by databank.php)
-M write debug files and preserve original .h3m and -o... file
-off single input map: put HeroWO files into the folder of
intermediate -o, do not create a subfolder
-oft no -off -nx: name output subfolders after map title, not file
Command line is the same as accepted by h3m2json.php, except output cannot be
stdout (-) because HeroWO maps are multi-file (regardless of the info below).
As with -of, -oft will overwrite files if map titles are not unique.
HELP;
$inherited = parent::helpText();
if ($inherited) {
$inherited = preg_split('/\r?\n\r?\n/u', $inherited);
$inherited = join(PHP_EOL.PHP_EOL, array_slice($inherited, 3, -2));
} else {
$inherited = <<<HELP
$this->scriptFile needs h3m2json.php. Download it to $this->scriptFile's folder or h3m2json$ds subfolder from:
https://github.com/HeroWO-js/h3m2json
HELP;
}
return $text.PHP_EOL.PHP_EOL.$inherited;
}
function parseArgv(array $argv) {
foreach (array_reverse($argv, true) as $i => $arg) {
switch ($arg) {
case '-d':
$this->databankPath = array_splice($argv, $i + 1, 1)[0];
break;
case '-M':
$this->debugFiles = true;
break;
case '-off':
$this->outputHeroWoSubfolder = false;
break;
case '-oft':
$this->outputHeroWoSubfolder = 'title';
break;
default:
continue 2;
}
array_splice($argv, $i, 1);
}
return parent::parseArgv($argv);
}
function run() {
if ($this->outputPath === '-') {
throw new ConvertError("Output path must be a file or folder, not stdout (-).");
}
if (!is_dir($this->databankPath)) {
fwrite($this->outputStream, $this->helpText());
return 1;
}
require_once __DIR__.'/databank.php';
$this->convertor = new class extends Convertor {
protected function readDatabank($file) {
return file_get_contents("$this->databankPath/$file");
}
};
$this->convertor->warner = function ($msg, $important) {
if ($this->failOnWarning and $important) {
throw new ConvertError("Warning treated as error (-ew): $msg");
} else {
fprintf($this->errorStream, "(*) %s%s", $msg, PHP_EOL);
}
};
$this->convertor->databankPath = $this->databankPath;
$this->convertor->loadDatabank();
return parent::run();
}
protected function processFile($inputPath, $outputPath, $autoOutputPath) {
$res = parent::processFile($inputPath, $outputPath, $autoOutputPath);
if (isset($res['h3m']) and !is_resource($inputPath)) {
extract($res); // overrides arguments of this method
$this->convertor->isTutorial = !strcasecmp(basename($inputPath), 'Tutorial.tut');
$builder = $res['builder'] = $this->convertor->fromH3M($h3m);
$builder->outputPath = $this->herowoSubfolder($outputPath, $h3m);
is_dir($builder->outputPath) or mkdir($builder->outputPath);
$builder->debugFiles = $this->debugFiles;
$builder->write();
// In non-M mode, delete the -o file (only needed for convertion). In -M mode,
// copy input to the output
// and move -o but only if not -off because it'd be already in $outputPath.
//
// Remember h3m2herowo.php doesn't allow $outputPath to be '-'.
if (!$this->debugFiles) {
unlink($outputPath);
} else {
copy($inputPath, $builder->outputPath.'/original.'.static::$formatToExtension[$inputFormat]);
if ($this->outputHeroWoSubfolder) {
rename($outputPath, $builder->outputPath.'/original.'.static::$formatToExtension[$outputFormat]);
}
}
}
return $res;
}
protected function herowoSubfolder($outputPath, H3M\H3M $h3m = null) {
if ($this->outputHeroWoSubfolder === true) {
// C:\foo\bar.json -> C:\foo\bar
//
// mkdir() fails on Windows if name has trailing whitespace (as
// present in "Pandora's Box .h3m").
return preg_replace('~\s*\.[^\\\\/]*$~u', '', $outputPath);
} else {
// C:\foo\bar.json -> C:\foo
$res = preg_replace('~[\\\\/][^\\\\/]*$~u', '', $outputPath);
if ($this->outputHeroWoSubfolder) { // 'title'
$res .= DIRECTORY_SEPARATOR.preg_replace('~[\\/:*?"<>|]~u', '', $h3m->name);
}
return $res;
}
}
protected function checkSkipConverted($outputPath, $inputPath) {
if ($this->skipConverted) {
if ($this->outputHeroWoSubfolder === 'title') {
throw new ConvertError("-oft is incompatible with -nx.");
} else {
return is_file($this->herowoSubfolder($outputPath, null).'/map.json');
}
}
}
}
abstract class Convertor {
const VERSION = 1;
public $warner;
public $isTutorial;
// XXX=I {Checks} only works for existing objects; it will fail when hero/monster was defeated
static $seerHutQuest = [
// SEERHUT.TXT[2]
H3M\Quest_Level::class => [
'I am old and wise, and I do not admit just anyone into my home. You may enter when you have reached `{Checks`}.',
'The reward I have is only for someone who is wise enough to handle it. Achieve `{Checks`} and I will reward you.',
// XXX=IC: hhqm: Slightly different message in SoD.
'I am old, and dying. Before I die I want to bequeath my possessions to someone worthy of them. Achieve `{Checks`} and I will know of your worth.',
],
// SEERHUT.TXT[7]
H3M\Quest_PrimarySkills::class => [
'I am a biographer of great heroes. I\'d really like to meet a hero who has mastered `{Checks`}. I\'d pay well for his story.',
'For those who have attained `{Checks`} there are great rewards. When you are finished return to me and I will see what can be done.',
'I am not likely to speak with anyone who is lesser than myself. If you have `{Checks`} then you will be better than I, and worthy of my attentions',
],
// SEERHUT.TXT[12]
H3M\Quest_DefeatHero::class => [
'I was once rich and famous, but `{Checks`} the terrible was my downfall. I lost my lands, I lost my title, and I lost my family. Please, bring the villain to justice.',
'Long ago I was in love, but `{Checks`} killed my sweetheart. Please, destroy this evil villian so I may live the rest of my life knowing my love\'s killer has been brought to justice.',
'We were driven from our home by `{Checks`}. If you could make sure they will never bother us again we would reward you greatly.',
],
// SEERHUT.TXT[17]
H3M\Quest_DefeatMonster::class => [
// XXX=IC:hhqm:
'This land is menaced by `{Checks`}. If you could be so bold as to defeat them, I would reward you richly.',
'A group of `{Checks`} have driven us from our homes. If you could drive them off we could go home, and would leave you with great rewards.',
'In order to get to my sick mother I have to get by `{Checks`} first. I am not a great warrior, but could reward you if the path was cleared.',
],
// SEERHUT.TXT[22]
H3M\Quest_Artifacts::class => [
'Long ago, powerful wizards were able to create magical artifacts, but time has caused us to forget how to make new items. I would like to learn these techniques myself, but I need one of these artifacts first to see how it was done. If you could bring me, `{Checks`}, you would be well rewarded.',
'I\'ve spent my life buying, selling, and collecting artifacts, but lately I\'ve been spending so much money acquiring new pieces I can hardly turn a profit. I think I might be able to start mass-producing artifacts, but I\'ve got to have one first to copy. If you could bring me `{Checks`}, I will reward your efforts.',
'In my younger days I\'d have done this myself, but I need your help. A friend of mine recently had a family heirloom stolen, and wants to find it. The problem is that it looks exactly like `{Checks`}. Please bring me any item that fits that description. Even if the artifact is not the family heirloom, I will reward your efforts.',
],
// SEERHUT.TXT[27]
H3M\Quest_Creatures::class => [
'I am an agent for an emperor of a distant land. Recently, his armies have fallen on hard times. If you could bring `{Checks`} to me, I could pay you handsomely.',
'In order to travel through these dangerous lands my envoy needs more backup. I hear `{Checks`} are excellent guards. If you were to bring them to me I would be deeply grateful',
'It is traditional for a groom to have an escort of `{Checks`} in order to go to his bride. We were attacked and most of my escort was killed. If you could persuade them to help me I would be very grateful.',
],
// SEERHUT.TXT[32]
H3M\Quest_Resources::class => [
'I am researching a way to turn base metals into gold, but I am short of materials for my workshop. If you could bring me `{Checks`}, I would be most grateful.',
'Please help the poor children of the area. If you could bring `{Checks`} we could pay to have homes built for them. I would be at your service.',
'Please help me! I was robbed on the way to my wedding, and without a dowry my future husband will not be able to accept me. If you could bring me `{Checks`} I would reward you.',
],
// SEERHUT.TXT[37]
H3M\Quest_BeHero::class => [
'What I have is for `{Databank heroes`, name`, %d`} alone. I shall give it to none other.',
],
// SEERHUT.TXT[42]
H3M\Quest_BePlayer::class => [
'I have a prize for those who fly the %s flag.',
],
];
static $seerHutProgress = [
// SEERHUT.TXT[3]
H3M\Quest_Level::class => [
// XXX=IC:hhqm:
'Faugh. You again. Come back when you are `{Checks`}, as I told you.',
// XXX=IC:hhqm:
'Not even close to `{Checks`}, leave me until you are there!',
// XXX=IC:hhqm:
'You are unworthy. Only someone who is `{Checks`} will be worthy enough.',
],
// SEERHUT.TXT[8]
H3M\Quest_PrimarySkills::class => [
'Have you found a great hero for me to interview? He must have reached `{Checks`}.',
'I am truly sorry, but you have not attained `{Checks`} and I will not help you until then.',
'You are still unworthy. Only someone with `{Checks`} will be better than I.',
],
// SEERHUT.TXT[13]
H3M\Quest_DefeatHero::class => [
'Oh, I wish you brought better news. It aches my heart that `{Checks`} still roams free.',
'Still, the murder of my love, `{Checks`} is left to freely wander the world.',
'`{Checks`} is still out there and can harm us. Not until they are gone will we leave.',
],
// SEERHUT.TXT[18]
H3M\Quest_DefeatMonster::class => [
// XXX=IC:hhqm:
'Don\'t lose heart. Defeating `{Checks`} is a difficult task, but you will surely succeed.',
// XXX=IC:hhqm:
'No, `{Checks`} have not been driven off. Until then we cannot go home.',
// XXX=IC:hhqm:
'My route is still infested with `{Checks`}. Please hurry, mother becomes more ill each day.',
],
// SEERHUT.TXT[23]
H3M\Quest_Artifacts::class => [
'Nothing, eh? I\'m sure you will find `{Checks`} soon. Please keep looking.',
'You still haven\'t found `{Checks`}? Well please keep looking, I lose money with each passing day!',
'Nothing yet? Ah well, keep trying, I\'m sure `{Checks`} is out there somewhere.',
],
// SEERHUT.TXT[28]
H3M\Quest_Creatures::class => [
// XXX=IC:hhqm:
'No luck in finding `{Checks`}? Please hurry, the empire depends on you.',
// XXX=IC:hhqm:
'I am sorry, but we really want `{Checks`} as guards.',
// XXX=IC:hhqm:
'No, those will simply not do. You must bring me `{Checks`} before I can go to my bride to be.',
],
// SEERHUT.TXT[33]
H3M\Quest_Resources::class => [
'Oh my, that\'s simply not enough. I need `{Checks`}. I\'ll never complete it with what you have.',
'Not unless all of `{Checks`} is donated we cannot build adequate homes for the orphans.',
'My dowry must contain all of `{Checks`} or I cannot get married.',
],
// SEERHUT.TXT[38]
H3M\Quest_BeHero::class => [
'You are not `{Databank heroes`, name`, %d`}. Begone!',
],
// SEERHUT.TXT[43]
H3M\Quest_BePlayer::class => [
'Your flag is not %s. I have nothing for you. Begone!',
],
];
static $seerHutComplete = [
// SEERHUT.TXT[4]
H3M\Quest_Level::class => [
// XXX=IC:hhqm:
'I thought you had promise. You have indeed reached `{Checks`}. Come in, come in. Here, I have something to reward you for your efforts. Do you accept?',
// XXX=IC:hhqm:
'Ahhh, you have reached `{Checks`}. Would you like to receive a reward?',
// XXX=IC:hhqm:
'Finally, there is someone to whom I can bequeath my worldly possessions, now that you have achieved `{Checks`} do you wish to inherit?',
],
// SEERHUT.TXT[9]
H3M\Quest_PrimarySkills::class => [
'I\'ve always wanted to meet someone as famous as you. Will you let me write down your life story?',
'You have reached `{Checks`}, as I knew you would. Are you ready to see the great rewards as a result?',
'It is a great thing to meet someone better than I. You have achieved `{Checks`}, will you accept the rewards of doing so?',
],
// SEERHUT.TXT[14]
H3M\Quest_DefeatHero::class => [
'I thought the day would never come! `{Checks`} is no more. Please, will you accept this reward?',
'Now I may continue with my sad life, that `{Checks`} has been brought to justice. For this comfort would you accept a reward?',
'We are finally able to return to our home, no that `{Checks`} has been defeated. Would you accept a reward as a token of our gratitude?',
],
// SEERHUT.TXT[19]
H3M\Quest_DefeatMonster::class => [
// XXX=IC:hhqm:
'At last, you defeated `{Checks`}, and the countryside is safe again! Are you ready to accept the reward?',
// XXX=IC:hhqm:
'Finally, `{Checks`} are gone from our home and we can return! Will you accept this reward?',
// XXX=IC:hhqm:
'The route is clear, I thank you deeply. Take this as a symbol of my gratitude.',
],
// SEERHUT.TXT[24]
H3M\Quest_Artifacts::class => [
'Ah, exactly what I needed! Here is the reward I promised. You still wish to trade `{Checks`}, yes?',
// XXX=IC:hhqm:
'Yes! `{Checks`} is perfect! Now if you\'ll kindly give it to me, I shall pay what I promised.',
'Yes, this might just be what we\'re looking for! May I please have `{Checks`}?',
],
// SEERHUT.TXT[29]
H3M\Quest_Creatures::class => [
'At last, the `{Checks`} that will save our empire! Here is your payment. Are they ready to depart?',
'Excellent! You have brought us the right amount of `{Checks`} as guards. Will you exchange them for a great reward?',
'Thank you so much kind travelor! I will give you a bountiful reward in exchange for the service of those `{Checks`}.',
],
// SEERHUT.TXT[34]
H3M\Quest_Resources::class => [
'Finally! Here, give the `{Checks`} to me, and I\'ll give you this in return.',
'Ahh, with all of the `{Checks`} we can build good homes for them. Would you accept this in return for your donation?',
'Thank you so much kind travelor! If you give me `{Checks`} I will give you a reward. Will you trade?',
],
// SEERHUT.TXT[39]
H3M\Quest_BeHero::class => [
'Finally! It is you, `{Databank heroes`, name`, %d`}. Here is what I have for you. Do you accept?',
],
// SEERHUT.TXT[44]
H3M\Quest_BePlayer::class => [
'Ah, one who bears the %s flag. Here is a prize for you. Do you accept?',
],
];
static $questGuardQuest = [
// SEERHUT.TXT[2]
H3M\Quest_Level::class => [
'The lands beyond are very dangerous. The guards eye you dubiously, but agree to let you by when you have achieved the `{Checks`}.',
// XXX=IC:hhqm:
'I am sorry, but this is a guildhouse, and only those who are experienced enough can join. Only those who are part of the guildhouse may pass. Until you reach `{Checks`}, you may not join.',
// XXX=IC:hhqm:
'We have a problem with our King. He doesn\'t like to be surrounded by immature people. Therefore you need to be of `{Checks`} in order to pass through.',
],
// SEERHUT.TXT[7]
H3M\Quest_PrimarySkills::class => [
'The guard post here is manned by retired heroes. They will not let you pass until you can prove you have mastered `{Checks`}.',
'Only those who have reached `{Checks`} are allowed to pass. Our reasons are our own.',
'A fair maiden languishes within the tower and only allows those who impress her to pass through. You would need a `{Checks`} in order to impress her.',
],
// SEERHUT.TXT[12]
H3M\Quest_DefeatHero::class => [
'The guards here protect the lands beyond from the depredations of `{Checks`}. They will not let anyone pass so long as the threat remains.',
'The guards were placed here in order to keep out `{Checks`}, a hero of great power and evil intentions towards their people. Until they are defeated no one shall pass.',
'The Queen wants `{Checks`} to be taught a lesson because they insulted her, calling her a fat old hag. Until this is done she has closed the borders.',
],
// SEERHUT.TXT[17]
H3M\Quest_DefeatMonster::class => [
// XXX=IC:hhqm:
'The Belted Knights of Erathia guard this tower. They will only let one of their own pass. To join the order, you must first defeat `{Checks`}.',
// XXX=IC:hhqm:
'Beware, `{Checks`} are running loose out there. We can\'t open the doors until each and every one is driven from the land.',
'Our doors do not open for anyone. Prove your loyalty by defeating our enemies, `{Checks`}. Only then will you be allowed to pass.',
],
// SEERHUT.TXT[22]
H3M\Quest_Artifacts::class => [
'A powerful wizard owns this tower. He refuses to let you pass unless you bring him `{Checks`}.',
'This gate can only be opened with a very special key. Bring back `{Checks`} and you will be able to pass.',
// XXX=IC:hhqm:
'A small, henpecked man preers over the gate. "No one may pass. My dog ate my wife\'s, `{Checks`}, and I\'m not leaving here until I find a replacement.',
],
// SEERHUT.TXT[27]
H3M\Quest_Creatures::class => [
'The King wants to see some `{Checks`}. In order for him to do so we need to look outside the kingdom. Bring us them and we\'ll let you through.',
'Each year during our Festival of Life we need some `{Checks`}. Bring some or don\'t bother coming back. It is the only way you will pass.',
'A mercenary troop occupies this tower. They say they will let you pass if you bring them `{Checks`} as recruits.',
],
// SEERHUT.TXT[32]
H3M\Quest_Resources::class => [
'The guards here are charging a toll of all travelers. They will let you pass for `{Checks`}.',
'All people must pay the King\'s Road Tax. It is `{Checks`}. Unless you pay it we will not let you pay.',
'We are quite sorry, but we refuse to move out of here and let you through. If you were to bring us `{Checks`} then we could move into another, comfortable home.',
],
// SEERHUT.TXT[37]
H3M\Quest_BeHero::class => [
'The guards here say they have orders to only let `{Databank heroes`, name`, %d`} pass.',
],
// SEERHUT.TXT[42]
H3M\Quest_BePlayer::class => [
'The guards here say they will only let those who fly the %s flag pass.',
],
];
static $questGuardProgress = [
// SEERHUT.TXT[3]
H3M\Quest_Level::class => [
// XXX=IC:hhqm:
'The guards here simply will not permit anyone below `{Checks`} to pass.',
// XXX=IC:hhqm:
'There is no way we\'re going to let a wimp like you into our guild. Not until you are of `{Checks`} can you join.',
// XXX=IC:hhqm:
'Only when you are of `{Checks`} will our King stand for your presence.',
],
// SEERHUT.TXT[8]
H3M\Quest_PrimarySkills::class => [
'The retired heroes set you a series of tests, which you fail miserably. Clearly you have not mastered `{Checks`}.',
'You have not reached `{Checks`}, go away.',
'She laughs because you have not yet reached `{Checks`}, and are not impressive.',
],
// SEERHUT.TXT[13]
H3M\Quest_DefeatHero::class => [
'The guards still fear `{Checks`}, so you cannot pass.',
'`{Checks`} is still running around on the loose.',
'`{Checks`} has still not yet been taught a lesson.',
],
// SEERHUT.TXT[18]
H3M\Quest_DefeatMonster::class => [
// XXX=IC:hhqm:
'The Belted Knights still will not let you pass, so you have not conquered `{Checks`}.',
// XXX=IC:hhqm:
'No, `{Checks`} are still running loose.',
'You have not yet proved your loyalty by defeating `{Checks`}. Leave us.',
],
// SEERHUT.TXT[23]
H3M\Quest_Artifacts::class => [
'The wizard is admant. Without `{Checks`}, none will pass.',
'You have not yet found the key, without `{Checks`} you cannot pass.',
'Sorry, that won\'t fool her. You need `{Checks`} in order to get me to leave.',
],
// SEERHUT.TXT[28]
H3M\Quest_Creatures::class => [
// XXX=IC:hhqm:
'I am sorry, but the King wants to only see `{Checks`}, nothing else will do.',
'Nothing but `{Checks`} will do for our Festival. Begone until you have them.',
'The mercenaries still require `{Checks`} before you may pass.',
],
// SEERHUT.TXT[33]
H3M\Quest_Resources::class => [
'Since you have not brought `{Checks`}, the guards forbid you passage.',
'That is not enough. The King\'s Road Tax is `{Checks`}.',
'For that pathetic amount we couldn\'t buy a shack. We\'ll need at least `{Checks`} in order to have a good home.',
],
// SEERHUT.TXT[38]
H3M\Quest_BeHero::class => [
'The guards here will only let `{Databank heroes`, name`, %d`} pass.',
],
// SEERHUT.TXT[43]
H3M\Quest_BePlayer::class => [
'The guards here will only let those who fly the %s flag pass.',
],
];
static $questGuardComplete = [
// SEERHUT.TXT[4]
H3M\Quest_Level::class => [
// XXX=IC:hhqm:
'The guards acknowledge that you have indeed reached `{Checks`}. Do you wish to pass at this time?',
// XXX=IC:hhqm:
'Now that you have reached `{Checks`} level you may join our guild. Membership is free. Do you wish to pass at this time?',
// XXX=IC:hhqm:
'Excellent! Now that you are of `{Checks`} our King will not have any problems with you. Do you wish to pass at this time?',
],
// SEERHUT.TXT[9]
H3M\Quest_PrimarySkills::class => [
'The retired heroes set you a series of tests, which you pass easily, demonstrating your mastery of `{Checks`}. Do you wish to pass?',
'You have reached `{Checks`}. Do you wish to pass?',
'She is very impressed by you because you have reached `{Checks`}. Do you wish to pass now?',
],
// SEERHUT.TXT[14]
H3M\Quest_DefeatHero::class => [
'Now that you have vanquished `{Checks`}, the threat is gone. Do you wish to pass?',
'Since `{Checks`} has been defeated and is no longer a threat the guards may let people pass. Do you wish to pass at this time?',
'You have taught `{Checks`} a lesson so the Queen will allow people to pass. Do you wish to at this time?',
],
// SEERHUT.TXT[19]
H3M\Quest_DefeatMonster::class => [
// XXX=IC:hhqm:
'News of your defeat of `{Checks`} traveled quickly. Do you wish to pass, oh newly Belted Knight?',
'Now that `{Checks`} are gone we can open our doors. Would you like to enter at this time?',
'Your loyalty has been proven by defeating `{Checks`}. Do you wish to pass at this time?',
],
// SEERHUT.TXT[24]
H3M\Quest_Artifacts::class => [
'The wizard agrees to let you by in exchange for `{Checks`}. Do wish to pass at this time?',
'Using `{Checks`} you may open the gate and pass through. Do you wish to pass at this time?',
// XXX=IC:hhqm:
'"Give it here and you can pass. Want a dog? Just kidding, will you give `{Checks`} to me?"',
],
// SEERHUT.TXT[29]
H3M\Quest_Creatures::class => [
'Excellent! You have found the `{Checks`} the King is so anxious to see. Let them go with us and you may pass.',
'Excellent! You may pass if you give us the `{Checks`}. Will you make the exchange now?',
'The mercenaries agree to let you pass in exchange for `{Checks`} as recruits. Do you wish to make the exchange now?',
],
// SEERHUT.TXT[34]
H3M\Quest_Resources::class => [
'The guards here are charging a toll of all travelers. They will let you pass for `{Checks`}. Do you wish to pay the toll?',
'If you give us the King\'s Road Tax of `{Checks`} we will let you pass. Do you agree?',
'Now that is the right kind of money. With `{Checks`} we can build or buy ourselves a nice place. You give it here and we let you pass, eh?',
],
// SEERHUT.TXT[39]
H3M\Quest_BeHero::class => [
'At last, it is `{Databank heroes`, name`, %d`}. Do you wish to pass?',
],
// SEERHUT.TXT[44]
H3M\Quest_BePlayer::class => [
'The guards note your %s flag and offer to let you pass. Do you accept?',
],
];
// Databank's data.
protected $nameToID;
protected $constants;
protected $producers;
protected $classes;
protected $heroes;
protected $buildings;
protected $creatures;
protected $spells;
protected $skills;
protected $towns;
protected $artifacts;
protected $hallBuildings;
protected $dwellings; // array of AClass->$id for dwelling class => array of Creature->$id that they produce
protected $terrainByH3; // SoD class '-' subclass => AClass->$id
protected $riverByH3;
protected $roadByH3;
protected $objectByH3;
//protected $o_OPERATION;
protected $stats = ['attack', 'defense', 'spellPower', 'knowledge'];
protected $h3m; // the input
protected $builder; // the output
protected $h3mObjectIDs;
// Array of regular 'hero' AObject-s (not randomHero or heroPlaceholder).
protected $heroObjects;
protected $resolveH3mObjectIDs;
protected $minX;
protected $minY;
protected $maxX;
protected $maxY;
protected $effectLabel = 0; // next available labeled Effect index
// function ( [$important = true,] $msg [, $formatArg1, ...] )
function warning($important) {
if ($this->warner) {
$args = func_get_args();
is_bool($important) ? array_shift($args) : $important = true;
call_user_func($this->warner, call_user_func_array('sprintf', $args),
$important, $this);
}
}
// Drops previously loaded databank, if any.
//
// It's okay to loadDatabank() just once when converting multiple maps (if
// they all use the same databank/modification),
function loadDatabank() {
$this->nameToID = [];
foreach (['constants', 'producers'] as $index) {
$this->$index = json_decode($this->readDatabank("$index.json"), true);
}
$stores = ['classes', 'heroes', 'buildings', 'creatures', 'spells',
'skills', 'towns', 'artifacts'];
foreach ($stores as $store) {
$this->$store = ObjectStore::from(json_decode($this->readDatabank("$store.json"), true));
}
foreach ($this->const('effect.operation') as $name => $value) {
$this->{"o_$name"} = $value;
}
$this->o_clamp00 = [$this->o_clamp, 0, 0];
$this->o_false = [$this->o_const, false];
// Unrolling is permanent and we assume if somebody runs several convertions
// per process, they are doing so with a compatible (same) databank.
if (!isset(Map::$unrolled['resources'])) {
unrollStores([
'constants' => $this->constants,
'artifactSlotsID' => $this->nameToID('artifactSlots'),
'buildingsID' => $this->nameToID('buildings'),
]);
// Part of databank.php, not handled by unrollStores().
Hero::$compact['artifacts']['strideX'] = max($this->nameToID('artifactSlots')) + 1;
}
$this->hallBuildings = [
$this->nameToID('buildings', 'hall'),
$this->nameToID('buildings', 'townHall'),
$this->nameToID('buildings', 'cityHall'),
$this->nameToID('buildings', 'capitol'),
];
$this->dwellings = $this->terrainByH3 = $this->riverByH3 = [];
$this->roadByH3 = $this->objectByH3 = [];
for ($id = 0; $id < $this->classes->x(); $id++) {
$type = $this->classes->atCoords($id, 0, 0, 'type');
if (AObject::type[$type] === 'dwelling') {
$this->dwellings[$id] = $this->classes->atCoords($id, 0, 0, 'produce');
}
switch ($type = AObject::type[$type]) {
default:
$type = 'object';
case 'terrain':
case 'river':
case 'road':
$class = $this->classes->atCoords($id, 0, 0, 'class');
$subclass = $this->classes->atCoords($id, 0, 0, 'subclass');
$this->{$type.'ByH3'}["$class-$subclass"][] = $id;
}
}
}
abstract protected function readDatabank($file);
// Fetches value of dot-separated $path resolving to a constant.
// const('resources.wood');
function const($path) {
$cur = $this->constants;
foreach (explode('.', $path) as $name) { $cur = $cur[$name]; }
return $cur;
}
// Shortcut for creating an $append modifier:
// o_append(123); //=> [$append, 123]
// o_append([1, 2, 3]); //=> [$append, 1, 2, 3]
protected function o_append($values) {
return array_merge([$this->o_append], (array) $values);
}
protected function effect(array $effect) {
$this->effects([$effect]);
}
protected function effects(array $effects) {
mergeInto($this->builder->effects, $effects);
}
// function ($index) - return parsed $index.json
// function ($index, $name) - return value for $name key, or throw
// function ($index, $name, $default) - for $name, or $default (and warn)
function nameToID($index, $name = null, $default = null) {
$ref = &$this->nameToID[$index];
$ref or $ref = json_decode($this->readDatabank("{$index}ID.json"), true);
if (!isset($name)) {
return $ref;
} elseif (isset($ref[$name])) {
return $ref[$name];
} elseif (func_num_args() > 2) {
$this->warning("cannot resolve '%s' in %s, assuming %s",
$name, $index, json_encode($default));
return $default;
} else {
throw new ConvertError("Cannot resolve '$name' in $index.");
}
}
// h3m2json.php tries to convert many well-known (standard) values to
// strings, like map difficulty. If a value is not well-known, it's stored as
// an integer. Since h3m2herowo.php doesn't support such values either, it
// tries to use default (fallback) values.
//
// known() checks value of $obj->$prop while knownValue() checks the immediate
// value, using $prop only for diagnostic messages. If $default is null,
// convertion is aborted with an exception.
protected function known(H3M\Structure $obj, $prop, $default = null) {
$args = [$prop, $obj->$prop];
func_num_args() > 2 and $args[] = $default;
return $this->knownValue(...$args);
}
// See known().
protected function knownValue($prop, $value, $default = null) {
if (is_int($value)) {
if (func_num_args() > 3) {
$this->warning("unknown '%s' value: %s, assuming: %s", $prop,
json_encode($value), json_encode($default));
$value = $default;
} else {
throw new ConvertError("Unknown '$prop' value: $value");
}
}
return $value;
}
// $id - index in the list of "object details" section of .h3m. Not $objectID
// as read by h3m2json.php.
protected function objectByH3mID($id, object $referrer = null) {
$ref = $this->h3mObjectIDs[$id] ?? null;
if (!is_int($id) or !$ref) {
$referrer and $referrer = ', referenced by '.get_class($referrer);
throw new ConvertError("Non-existing object: #$id$referrer.");
}
return $ref;
}
// Kickstarts the actual convertion from .h3m to HeroWO's bunch-o'-JSON.
function fromH3M(H3M\H3M $h3m) {
if ($this->nameToID === null) {
throw new ConvertError('Call loadDatabank() before fromH3M().');
}
if ($h3m->_version !== static::VERSION) {
// Version of databank format should also match but we're implying it
// does because this script is distributed along with databank scripts.
$this->warning("%s is designed for map version %s but your h3m2json.php produces version %s; this may cause problems",
(new CLI)->scriptFile, static::VERSION, $h3m->_version);
}
$this->h3m = $h3m;
$builder = $this->builder = new MapBuilder;
$this->h3mObjectIDs = $this->heroObjects = $this->resolveH3mObjectIDs = [];
$map = $builder->map = new Map;
$map->modules = ['H3'];
$map->width = $h3m->size;
$map->height = $h3m->size;
$map->levels = $h3m->twoLevels + 1;
$map->origin = $this->known($h3m, 'format', "_".dechex((int) $h3m->format));
// Using map's difficulty as the default for player's difficulty mode.
$map->difficulty = $map->difficultyMode = $this->const('map.difficulty.'.$this->known($h3m, 'difficulty', 'normal'));
// SoD shows "Unnamed" in main menu (list of maps) and blank string in in-game Scenario Information.
$map->title = $h3m->name ?? 'Unnamed';
$map->description = $h3m->description ?? '';
$map->constants = $this->constants;
// Ignoring H3M->$isPlayable, $sizeText.
// First convert map objects, starting with tiles.
foreach ($h3m->overworldTiles as $i => $tile) {
$this->fromH3mTile($tile, $i, 0);
}
foreach ($h3m->underworldTiles ?: [] as $i => $tile) {
$this->fromH3mTile($tile, $i, 1);
}
// Now regular objects coming in.
$this->minX = $this->minY = 0;
$this->maxX = $this->maxY = $h3m->size - 1;
foreach ($h3m->objects as $id => $object) {
$this->fromH3mObject($object, $id);
}
$this->addMapMargin();
// Converting rest of map data - players, heroes, etc.
// Final object IDs are now available.
$obj = $map->players[] = new MapPlayer;
$obj->player = $this->nameToID('players', 'neutral');
$obj->controllers = [['type' => 'neutralAI']];
$onlyHuman = $otherHuman = null;
if (!array_filter(array_column($h3m->players, 'canBeHuman'))) {
$this->warning('no human players, enabling $canBeHuman for Red');
$h3m->players['red']->canBeHuman = true;
}
foreach ($h3m->players as $color => $player) {
if ($player->canBeHuman) {
$player->canBeComputer or $onlyHuman = $onlyHuman ?? count($map->players);
$otherHuman = $otherHuman ?? count($map->players);
}
$this->fromH3mPlayer($player, $color);
}
// Set default controller for players that can be both human and CPU to CPU, except setting the
// first such player to human if there are no human-only players.
// This allows starting new single-player game without configuration.
foreach ($map->players as $i => $player) {
if (count($player->controllers) === 2) { // 0 human, 1 ai
$player->controller = $i === ($onlyHuman ?? $otherHuman) ? 0 : 1;
}
}
foreach ($h3m->heroes as $id => $hero) {
$this->fromH3mCustomHero($hero, $id);
}
$this->fromH3mVictoryCondition($h3m->victoryCondition);
$this->fromH3mLossCondition($h3m->lossCondition);
$this->fromH3mChances();
$this->fromH3mHeroChances(); // handles Hero->$players (of $h3m->heroes)
$this->fromH3mRumors($h3m->rumors);
foreach ($h3m->events as $event) {
$this->fromH3mEvent($event);
}
if ($this->isTutorial) {
$event = new H3M\Event;
$res = $event->resources = new H3M\Resources;
// So that Red has 50 each resource and 50k gold.
$res->wood = $res->ore = 30;
$res->mercury = $res->sulfur = $res->crystal = $res->gems = 40;
$res->gold = 30000;
$event->players = ['red'];
$event->firstDay = 0;
$this->fromH3mEvent($event);
}
foreach ($this->resolveH3mObjectIDs as &$ref) {
list($obj, $prop, $h3mID) = $ref;
$id = $this->objectByH3mID($h3mID, $obj)->id;
// Preserve reference in [1], as in fromH3m_QuestGuard().
isset($obj) ? $obj->$prop = $id : $ref[1] = $id;
if ($prop === 'visiting' or $prop === 'garrisoned') {
$builder->objects[$id]->$prop = $obj->id; // assign hero's $visiting
}
}
// Do this after resolving IDs because finishH3mObjects() reads AObject->$visiting
// of heroes but it's only set above.
$this->finishH3mObjects();
// This is stored in staticEffectsSchema.json
// but it matches H3Effect::$normalize that we have available so take that.
Effect::$normalize = H3Effect::$normalize;
// Lastly, combine databank's static Effects with map-specific ones.
$builder->effects = array_merge(
H3Effect::fromShort(
$builder->effects,
[],
[
'priority' => $this->const('effect.priority.mapSpecific'),
]
),
array_map(
function ($effect) {
return new H3Effect($effect);
},
json_decode(file_get_contents("$this->databankPath/staticEffects.json"), true)
)
);
$builder->labeledEffects = array_merge(
array_map(function (array $effects) {
return H3Effect::fromShort($effects);
}, $builder->labeledEffects),
array_map(
function (array $effects) {
return array_map(function ($effect) {
return new H3Effect($effect);
}, $effects);
},
json_decode(file_get_contents("$this->databankPath/staticLabeledEffects.json"), true)
)
);
$builder->originalIDs = array_column($this->h3mObjectIDs, 'id');
$builder->classes = $this->classes;
return $builder;
}
protected function fromH3mPlayer(H3M\Player $player, $color) {
$obj = $this->builder->map->players[] = new MapPlayer;
$obj->player = $this->nameToID('players', $color);
$obj->team = $player->team + 1;
$obj->maxLevel = $this->h3m->maxHeroLevel;
$player->canBeHuman and $obj->controllers[] = ['type' => 'human'];
if ($player->canBeComputer) {
$beh = $this->known($player, 'behavior', 'random');
$obj->controllers[] = ['type' => 'ai'] +
($this->isTutorial ? ['behavior' => 'nop'] : []) +
($beh === 'random' ? [] : ['behavior' => $beh]);
}
// SoD Complete allows Conflux even in scenarios created for earlier versions (e.g. for AB). So do we.
foreach ($player->towns as $town) {
if (null !== $town = $this->knownValue('towns', $town, null)) {
$obj->towns[] = $this->nameToID('towns', $town);
}
}
if (is_int($id = $player->startingTown->object ?? null)) {
$obj->startingTown = $this->objectByH3mID($id, $player->startingTown)->id;
}
$this->fromH3mPlayerStartingHero($obj, $player);
// Ignoring $customizedTowns since $towns reflects its state.
// Ignoring $placeholderHeroes and $heroes, have no use for that.
}
protected function fromH3mPlayerStartingHero(MapPlayer $player, H3M\Player $h3mPlayer) {
if ($player->startingTown and $h3mPlayer->startingTown->createHero) {
$createInTown = $this->builder->objects[$player->startingTown];
if ($createInTown->visiting) {
$this->warning("not generating starting hero because another hero is visiting #%d", $createInTown->id);
} else {
$obj = $this->newObject(null, [
'class' => $this->nameToID('objects', 'randomHero')[0],
'x' => $createInTown->x + 2, // adjust to town's actionable spot
'y' => $createInTown->y + 4,
'z' => $createInTown->z,
'owner' => $player->player,
'visiting' => $createInTown->id,
]);
$object = $this->h3m->objects[$h3mPlayer->startingTown->object];
// XXX+R: dor:
$obj->displayOrder = $this->objectDisplayOrder($obj, $object, $object->index + 1);
$createInTown->visiting = $obj->id;
}
}
$randomHero = $fixedHero = null;
foreach ($this->builder->objects as $obj) {
if ($obj->owner === $player->player and
in_array($obj->class, $this->nameToID('objects', 'randomHero'))) {
$randomHero = $obj;
break;
}
}
// Since we don't know the exact algorithm SoD's editor uses for
// determining starting hero, we can try relying on $type. This should work
// as long as GH is off and there are no random heroes (but if any of this
// is false then $fixedHero won't be used anyway), and there are no heroes
// with duplicate identities (diminishingly rare, if possible at all).
foreach ($this->heroObjects as $obj) {
if ($obj->owner === $player->player and
// Set $fixedHero to the first hero object owned by player. However,
// if there is another hero object with $subclass matching H3M's $type
// then use the first one such hero.
(!$fixedHero or
$found = $obj->subclass === $h3mPlayer->startingHero->type)) {
$fixedHero = $obj;
if (!empty($found)) { break; }
}
}
if ($randomHero) {
$player->startingHero = $randomHero->id;
// Match Hero->$id.
$player->startingHeroClasses = $this->h3m->startingHeroes;
} elseif ($fixedHero) {
// If there is a hero but it's not random and Generate Hero is unset,