/
Place.cs
1671 lines (1465 loc) · 73.1 KB
/
Place.cs
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
// Project: Daggerfall Unity
// Copyright: Copyright (C) 2009-2022 Daggerfall Workshop
// Web Site: http://www.dfworkshop.net
// License: MIT License (http://www.opensource.org/licenses/mit-license.php)
// Source Code: https://github.com/Interkarma/daggerfall-unity
// Original Author: Gavin Clayton (interkarma@dfworkshop.net)
// Contributors:
//
// Notes:
//
using UnityEngine;
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using DaggerfallWorkshop;
using DaggerfallWorkshop.Utility;
using DaggerfallConnect;
using DaggerfallConnect.Arena2;
using DaggerfallWorkshop.Game.UserInterfaceWindows;
using FullSerializer;
using DaggerfallWorkshop.Game.Banking;
using DaggerfallWorkshop.Game.Guilds;
using DaggerfallWorkshop.Game.Serialization;
namespace DaggerfallWorkshop.Game.Questing
{
/// <summary>
/// A site involved in a quest.
/// Can be a random local/remote location or a fixed permanent location.
/// </summary>
public class Place : QuestResource
{
#region Fields
const int editorFlatArchive = 199;
const int spawnMarkerFlatIndex = 11;
const int itemMarkerFlatIndex = 18;
Scopes scope; // Fixed/remote/local
string name; // Source name for data table
int p1; // Parameter 1
int p2; // Parameter 2
int p3; // Parameter 3
SiteDetails siteDetails; // Site found using inputs
#endregion
#region Enums
public enum Scopes
{
None,
Fixed,
Remote,
Local,
}
#endregion
#region Properties
/// <summary>
/// Gets the scope of this Place.
/// </summary>
public Scopes Scope
{
get { return scope; }
}
/// <summary>
/// Gets the Name of this Place used for data table lookup.
/// </summary>
public string Name
{
get { return name; }
}
/// <summary>
/// Gets parameter 1 of Place.
/// </summary>
public int Param1
{
get { return p1; }
}
/// <summary>
/// Gets parameter 2 of Place.
/// </summary>
public int Param2
{
get { return p2; }
}
/// <summary>
/// Gets parameter 3 of Place.
/// </summary>
public int Param3
{
get { return p3; }
}
/// <summary>
/// Gets or sets full site details of Place.
/// </summary>
public SiteDetails SiteDetails
{
get { return siteDetails; }
set { siteDetails = value; }
}
#endregion
#region Constructors
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="parentQuest">Parent quest.</param>
public Place(Quest parentQuest)
: base(parentQuest)
{
}
/// <summary>
/// Construct a Place resource from QBN input.
/// </summary>
/// <param name="parentQuest">Parent quest.</param>
/// <param name="line">Place definition line from QBN.</param>
public Place(Quest parentQuest, string line)
: base(parentQuest)
{
SetResource(line);
}
#endregion
#region Overrides
public override void SetResource(string line)
{
base.SetResource(line);
// Match string for Place variants
string matchStr = @"(Place|place) (?<symbol>[a-zA-Z0-9_.-]+) (?<siteType>local|remote|permanent) (?<siteName>\w+)|" +
@"(Place|place) (?<symbol>[a-zA-Z0-9_.-]+) (?<siteType>randompermanent) (?<siteList>[a-zA-Z0-9_.,]+)";
// Try to match source line with pattern
bool randomSiteList = false;
Match match = Regex.Match(line, matchStr);
if (match.Success)
{
// Store symbol for quest system
Symbol = new Symbol(match.Groups["symbol"].Value);
// Get place scope
string siteType = match.Groups["siteType"].Value;
if (string.Compare(siteType, "local", true) == 0)
{
// This is a local place
scope = Scopes.Local;
}
else if (string.Compare(siteType, "remote", true) == 0)
{
// This is a remote place
scope = Scopes.Remote;
}
else if (string.Compare(siteType, "permanent", true) == 0)
{
// This is a permanent place
scope = Scopes.Fixed;
}
else if (string.Compare(siteType, "randompermanent", true) == 0)
{
// This is a comma-separated list of random sites
scope = Scopes.Fixed;
randomSiteList = true;
}
else
{
throw new Exception(string.Format("Place found no site type match found for source: '{0}'. Must be local|remote|permanent.", line));
}
// Get place name for parameter lookup
name = match.Groups["siteName"].Value;
if (string.IsNullOrEmpty(name) && !randomSiteList)
{
throw new Exception(string.Format("Place site name empty for source: '{0}'", line));
}
// Pick one permanent place from random site list
if (randomSiteList)
{
string srcSiteList = match.Groups["siteList"].Value;
string[] siteNames = srcSiteList.Split(',');
if (siteNames == null || siteNames.Length == 0)
throw new Exception(string.Format("Place randompermanent must have at least one site name in source: '{0}'", line));
name = siteNames[UnityEngine.Random.Range(0, siteNames.Length)];
}
// Try to read place variables from data table
Table placesTable = QuestMachine.Instance.PlacesTable;
if (placesTable.HasValue(name))
{
// Store values
p1 = CustomParseInt(placesTable.GetValue("p1", name));
p2 = CustomParseInt(placesTable.GetValue("p2", name));
p3 = CustomParseInt(placesTable.GetValue("p3", name));
}
else
{
throw new Exception(string.Format("Could not find place name in data table: '{0}'", name));
}
// Handle place by scope
if (scope == Scopes.Local)
{
// Get a local site from same town quest was issued
SetupLocalSite(line);
}
else if (scope == Scopes.Remote)
{
// Get a remote site in same region quest was issued
SetupRemoteSite(line);
}
else if (scope == Scopes.Fixed && p1 > 0x300)
{
// Get a fixed site, such as a capital city or dungeon
SetupFixedLocation();
}
else
{
throw new Exception("Invalid placeType in line: " + line);
}
}
}
/// <summary>
/// Expand macro for a Place.
/// </summary>
/// <param name="macro">Macro type to expand.</param>
/// <param name="text">Expanded text for this macro type. Empty if macro cannot be expanded.</param>
/// <returns>True if macro expanded, otherwise false.</returns>
public override bool ExpandMacro(MacroTypes macro, out string textOut)
{
// Store this place in quest as last Place encountered
// This will be used for %di, etc.
ParentQuest.LastPlaceReferenced = this;
textOut = string.Empty;
bool result = true;
switch (macro)
{
case MacroTypes.NameMacro1: // Name of house/business (e.g. Odd Blades)
textOut = siteDetails.buildingName;
break;
case MacroTypes.NameMacro2: // Name of location/dungeon (e.g. Gothway Garden)
textOut = TextManager.Instance.GetLocalizedLocationName(siteDetails.mapId, siteDetails.locationName);
break;
case MacroTypes.NameMacro3: // Name of dungeon (e.g. Privateer's Hold) - Not sure about this one, need to test
textOut = TextManager.Instance.GetLocalizedLocationName(siteDetails.mapId, siteDetails.locationName);
break;
case MacroTypes.NameMacro4: // Name of region (e.g. Tigonus)
textOut = TextManager.Instance.GetLocalizedRegionName(siteDetails.regionIndex);
break;
default: // Macro not supported
result = false;
break;
}
return result;
}
#endregion
#region Public Methods
/// <summary>
/// Sets up Place resource directly from current player location.
/// Player must currently be in a town or dungeon location.
/// </summary>
/// <returns>True if location configured or false if could not be configured (e.g. player not in town or dungeon).</returns>
public bool ConfigureFromPlayerLocation(string symbolName)
{
// Must have a location
if (!GameManager.Instance.PlayerGPS.HasCurrentLocation)
return false;
// Get player current location
DFLocation location = GameManager.Instance.PlayerGPS.CurrentLocation;
// Get current site type
SiteTypes siteType;
int buildingKey = 0;
string buildingName = string.Empty;
switch (GameManager.Instance.PlayerEnterExit.WorldContext)
{
case WorldContext.Dungeon:
siteType = SiteTypes.Dungeon;
buildingName = GameManager.Instance.PlayerEnterExit.Dungeon.GetSpecialDungeonName();
break;
case WorldContext.Interior:
siteType = SiteTypes.Building;
buildingKey = GameManager.Instance.PlayerEnterExit.BuildingDiscoveryData.buildingKey;
buildingName = GameManager.Instance.PlayerEnterExit.BuildingDiscoveryData.displayName;
break;
case WorldContext.Exterior:
siteType = SiteTypes.Town;
break;
default:
return false;
}
// Configure new site details
siteDetails = new SiteDetails();
siteDetails.questUID = ParentQuest.UID;
siteDetails.siteType = siteType;
siteDetails.mapId = location.MapTableData.MapId;
siteDetails.locationId = location.Exterior.ExteriorData.LocationId;
siteDetails.regionIndex = location.RegionIndex;
siteDetails.buildingKey = buildingKey;
siteDetails.buildingName = buildingName;
siteDetails.regionName = location.RegionName;
siteDetails.locationName = location.Name;
siteDetails.questSpawnMarkers = null;
siteDetails.questItemMarkers = null;
// Assign symbol
Symbol = new Symbol(symbolName);
return true;
}
/// <summary>
/// Selects a site marker for quest resource.
/// Initial selection will be to a fresh Quest or Item marker based on incoming resource.
/// Future selection to same site will be to previously selected marker.
/// If site has only a single Quest or Item marker, the best available marker will be used.
/// </summary>
/// <param name="resource">Incoming resource type about to be added.</param>
/// <param name="markerIndex">Static index to use. Must be able to find preferred marker type. Used by main quest only.</param>
/// <param name="markerPreference">Marker preference when using static index.</param>
bool GetSiteMarker(QuestResource resource, int markerIndex = -1, MarkerPreference markerIndexPreference = MarkerPreference.Default)
{
// Direct to previously selected marker, unless specific index specified
if (siteDetails.selectedMarker.targetResources != null)
{
if (markerIndex > -1)
{
// Specific index specified and already got the main selected marker, so just assign directly to marker list
if (resource is Person || resource is Foe || markerIndexPreference == MarkerPreference.UseQuestMarker)
AssignResourceToMarker(resource.Symbol.Clone(), ref siteDetails.questSpawnMarkers[markerIndex]);
else if (resource is Item)
AssignResourceToMarker(resource.Symbol.Clone(), ref siteDetails.questItemMarkers[markerIndex]);
return false;
}
return true;
}
// Handle "anymarker" preference
if (markerIndexPreference == MarkerPreference.AnyMarker)
{
// Create a combined list of all markers
List<QuestMarker> allMarkers = new List<QuestMarker>();
allMarkers.AddRange(siteDetails.questSpawnMarkers);
allMarkers.AddRange(siteDetails.questItemMarkers);
// Select a random marker from combined list
if (allMarkers.Count > 0)
{
siteDetails.selectedMarker = allMarkers[UnityEngine.Random.Range(0, allMarkers.Count)];
return true;
}
else
{
return false;
}
}
// Determine preferred marker type for this resource
MarkerTypes preferredMarkerType = MarkerTypes.None;
if (resource is Person || resource is Foe || markerIndexPreference == MarkerPreference.UseQuestMarker)
preferredMarkerType = MarkerTypes.QuestSpawn;
else if (resource is Item)
preferredMarkerType = MarkerTypes.QuestItem;
// Need to create a site marker - what do we have available?
bool hasSpawnMarker = siteDetails.questSpawnMarkers != null && siteDetails.questSpawnMarkers.Length > 0;
bool hasItemMarker = siteDetails.questItemMarkers != null && siteDetails.questItemMarkers.Length > 0;
// Assign to preferred marker if possible
if (preferredMarkerType == MarkerTypes.QuestSpawn && hasSpawnMarker)
{
// Assign to Spawn marker - supports marker index
if (markerIndex == -1)
siteDetails.selectedMarker = siteDetails.questSpawnMarkers[UnityEngine.Random.Range(0, siteDetails.questSpawnMarkers.Length)];
else
siteDetails.selectedMarker = siteDetails.questSpawnMarkers[markerIndex];
}
else if (preferredMarkerType == MarkerTypes.QuestItem && hasItemMarker)
{
// Assign to Item marker - supports marker index
if (markerIndex == -1)
siteDetails.selectedMarker = siteDetails.questItemMarkers[UnityEngine.Random.Range(0, siteDetails.questItemMarkers.Length)];
else
siteDetails.selectedMarker = siteDetails.questItemMarkers[markerIndex];
}
else if (hasSpawnMarker)
{
// Assign to any Spawn marker - does not support marker index
siteDetails.selectedMarker = siteDetails.questSpawnMarkers[UnityEngine.Random.Range(0, siteDetails.questSpawnMarkers.Length)];
}
else if (hasItemMarker)
{
// Assign to any Item marker - does not support marker index
siteDetails.selectedMarker = siteDetails.questItemMarkers[UnityEngine.Random.Range(0, siteDetails.questItemMarkers.Length)];
}
return true;
}
/// <summary>
/// Assigns a quest resource to this Place site.
/// Supports Persons, Foes, Items from within same quest as Place.
/// Quest must have previously created SiteLink for layout builders to discover assigned resources.
/// </summary>
/// <param name="targetSymbol">Resource symbol of Person, Item, or Foe to assign.</param>
/// <param name="markerIndex">Preferred marker index to use instead of random. Must be able to find preferred marker type. Used by main quest only.</param>
public void AssignQuestResource(Symbol targetSymbol, int markerIndex = -1, MarkerPreference markerIndexPreference = MarkerPreference.Default, bool cullExisting = true)
{
// Site must have at least one marker available
if (!ValidateQuestMarkers(siteDetails.questSpawnMarkers, siteDetails.questItemMarkers))
throw new Exception(string.Format("Tried to assign resource {0} to Place without at least a spawn or item marker.", targetSymbol.Name));
// Attempt to get resource from symbol
QuestResource resource = ParentQuest.GetResource(targetSymbol);
if (resource == null)
throw new Exception(string.Format("Could not locate quest resource with symbol {0}", targetSymbol.Name));
// Remove this resource if already injected at another Place
// Sometimes a quest will move a resource part way through quest
// This ensures resource does not become duplicated in both sites
if (cullExisting)
QuestMachine.Instance.CullResourceTarget(resource, Symbol);
// Get marker for new resource and assign resource to marker
if (GetSiteMarker(resource, markerIndex, markerIndexPreference))
AssignResourceToMarker(targetSymbol.Clone(), ref siteDetails.selectedMarker);
// Output debug information
if (markerIndexPreference == MarkerPreference.AnyMarker)
{
Debug.LogFormat("Assigned resource {0} to random quest or item marker", resource.Symbol.Name);
}
else if (resource is Person)
{
if (siteDetails.siteType == SiteTypes.Building)
Debug.LogFormat("Assigned Person {0} to Building {1}", (resource as Person).DisplayName, SiteDetails.buildingName);
else if (siteDetails.siteType == SiteTypes.Dungeon)
Debug.LogFormat("Assigned Person {0} to Dungeon {1}", (resource as Person).DisplayName, SiteDetails.locationName);
}
else if (resource is Foe)
{
if (siteDetails.siteType == SiteTypes.Building)
Debug.LogFormat("Assigned Foe _{0}_ to Building {1}", resource.Symbol.Name, SiteDetails.buildingName);
else if (siteDetails.siteType == SiteTypes.Dungeon)
{
if (markerIndex == -1)
Debug.LogFormat("Assigned Foe _{0}_ to Dungeon {1}", resource.Symbol.Name, SiteDetails.locationName);
else
Debug.LogFormat("Assigned Foe _{0}_ to Dungeon {1}, index {2}", resource.Symbol.Name, SiteDetails.locationName, markerIndex);
}
}
else if (resource is Item)
{
if (siteDetails.siteType == SiteTypes.Building)
{
Debug.LogFormat("Assigned Item _{0}_ to Building {1}", resource.Symbol.Name, SiteDetails.buildingName);
}
else if (siteDetails.siteType == SiteTypes.Dungeon)
{
if (markerIndex == -1)
Debug.LogFormat("Assigned Item _{0}_ to Dungeon {1}", resource.Symbol.Name, SiteDetails.locationName);
else
Debug.LogFormat("Assigned Item _{0}_ to Dungeon {1}, index {2}", resource.Symbol.Name, SiteDetails.locationName, markerIndex);
}
}
// Hot-place resource if player already at this Place at time of placement
// This means PlayerEnterExit could not have called placement at time of assignment
// e.g. M0B30Y08 places zombie only when player already inside building after 7pm
if (IsPlayerHere())
{
// Get component handling player world status and transitions
PlayerEnterExit playerEnterExit = GameManager.Instance.PlayerEnterExit;
if (!playerEnterExit)
return;
if (playerEnterExit.IsPlayerInsideBuilding)
{
GameObjectHelper.AddQuestResourceObjects(SiteTypes.Building, playerEnterExit.Interior.transform, playerEnterExit.Interior.EntryDoor.buildingKey);
}
else if (playerEnterExit.IsPlayerInsideDungeon)
{
GameObjectHelper.AddQuestResourceObjects(SiteTypes.Dungeon, playerEnterExit.Dungeon.transform);
}
}
// Hot-remove resource if moved somewhere player is not
if (!IsPlayerHere() && resource.QuestResourceBehaviour)
{
GameObject.Destroy(resource.QuestResourceBehaviour.gameObject);
}
}
/// <summary>
/// Checks if player is at this place.
/// </summary>
/// <returns>True if player at this place.</returns>
public bool IsPlayerHere()
{
bool result = false;
// Check building site
if (SiteDetails.siteType == SiteTypes.Building)
result = CheckInsideBuilding(this);
else if (SiteDetails.siteType == SiteTypes.Town)
result = CheckInsideTown(this);
else if (SiteDetails.siteType == SiteTypes.Dungeon)
result = CheckInsideDungeon(this);
return result;
}
/// <summary>
/// Called for each Place resource after Quest has finished loading.
/// Required to map old resource marker allocations to new marker resource system.
/// </summary>
public void ReassignSiteDetailsLegacyMarkers()
{
if (siteDetails.selectedMarker.targetResources == null)
{
// Get all prior resource symbols placed by this quest
// Clear legacy resource symbols from old markers after collecting them
List<Symbol> resources = new List<Symbol>();
if (siteDetails.questSpawnMarkers != null && siteDetails.questSpawnMarkers.Length > 0)
{
if (siteDetails.questSpawnMarkers[siteDetails.selectedQuestSpawnMarker].targetResources != null)
{
resources.AddRange(siteDetails.questSpawnMarkers[siteDetails.selectedQuestSpawnMarker].targetResources.ToArray());
siteDetails.questSpawnMarkers[siteDetails.selectedQuestSpawnMarker].targetResources.Clear();
}
}
if (siteDetails.questItemMarkers != null && siteDetails.questItemMarkers.Length > 0)
{
if (siteDetails.questItemMarkers[siteDetails.selectedQuestItemMarker].targetResources != null)
{
resources.AddRange(siteDetails.questItemMarkers[siteDetails.selectedQuestItemMarker].targetResources.ToArray());
siteDetails.questItemMarkers[siteDetails.selectedQuestItemMarker].targetResources.Clear();
}
}
// Assign these to new marker setup
foreach (Symbol symbol in resources)
{
QuestResource resource = ParentQuest.GetResource(symbol);
if (resource != null)
{
// Get previous marker index
int previousMarkerIndex = -1;
if (resource is Person || resource is Foe)
previousMarkerIndex = siteDetails.selectedQuestSpawnMarker;
else if (resource is Item)
previousMarkerIndex = siteDetails.selectedQuestItemMarker;
// Reassign to same marker
AssignQuestResource(symbol, previousMarkerIndex, MarkerPreference.Default, false);
Debug.LogFormat("Reassigned resource {0} to new marker system.", symbol.Original);
}
}
}
}
/// <summary>
/// Custom parser to handle hex or decimal values from places data table.
/// </summary>
public static int CustomParseInt(string value)
{
int result;
if (value.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
{
result = int.Parse(value.Replace("0x", ""), NumberStyles.HexNumber);
}
else
{
result = int.Parse(value);
}
return result;
}
static readonly int[] validBuildingTypes = { 0, 2, 3, 5, 6, 8, 9, 11, 12, 13, 14, 15, 17, 18, 19, 20 };
static readonly int[] validHouseTypes = { 17, 18, 19, 20 };
static readonly int[] validShopTypes = { 0, 2, 5, 6, 7, 8, 9, 12, 13 }; // not including bank and library
public static bool IsPlayerAtBuildingType(int p2, int p3)
{
// Get component handling player world status and transitions
PlayerEnterExit playerEnterExit = GameManager.Instance.PlayerEnterExit;
if (!playerEnterExit)
return false;
if (!playerEnterExit.IsPlayerInsideBuilding)
return false;
int buildingType = (int)playerEnterExit.Interior.BuildingData.BuildingType;
// DFU extensions
if (p2 == -1)
{
switch(p3)
{
case 0: // random
return validBuildingTypes.Contains(buildingType);
case 1: // house
return validHouseTypes.Contains(buildingType);
case 2: // shop
return validShopTypes.Contains(buildingType);
default: // unhandled
Debug.LogWarning($"Unhandled building type: p1=0, p2={p2}, p3={p3}");
return false;
}
}
// Handle guild halls
else if (p2 == (int)DFLocation.BuildingTypes.GuildHall)
{
if (buildingType != p2)
return false;
// Accept any guild hall
if (p3 == 0)
return true;
// Faction id must match
return playerEnterExit.FactionID == p3;
}
else
{
return buildingType == p2;
}
}
#endregion
#region Local Site Methods
/// <summary>
/// Get a local building in the town player is currently at.
/// Throws exception if no valid buildings of specified type are found.
/// Example of how this can happen is issuing a quest to a local palace in a town with no palace.
/// Use remote palace instead to ensure quest can select from entire region.
/// </summary>
void SetupLocalSite(string line)
{
// Daggerfall has no local dungeons but some quests (e.g. Sx007) can request one
// This is used to stash a resource somewhere player cannot find it
// Setup a remote dungeon instead
if (p1 == 1)
{
SetupRemoteSite(line);
return;
}
// Get player location
DFLocation location = GameManager.Instance.PlayerGPS.CurrentLocation;
if (!location.Loaded)
throw new Exception("Tried to setup a local site but player is not in a location (i.e. player in wilderness).");
// Get list of valid sites
SiteDetails[] foundSites = null;
if (p2 == -1 && p3 == 0)
foundSites = CollectQuestSitesOfBuildingType(location, DFLocation.BuildingTypes.AllValid, p3);
else if (p2 == -1 && p3 == 1)
foundSites = CollectQuestSitesOfBuildingType(location, DFLocation.BuildingTypes.AnyHouse, p3);
else if (p2 == -1 && p3 == 2)
foundSites = CollectQuestSitesOfBuildingType(location, DFLocation.BuildingTypes.AnyShop, p3);
else
foundSites = CollectQuestSitesOfBuildingType(location, (DFLocation.BuildingTypes)p2, p3);
// Do some fallback for house types if nothing found locally
// There should almost always be a suitable local house available
DFLocation.BuildingTypes requiredBuildingType = (DFLocation.BuildingTypes)p2;
if ((foundSites == null || foundSites.Length == 0) &&
requiredBuildingType >= DFLocation.BuildingTypes.House1 &&
requiredBuildingType <= DFLocation.BuildingTypes.House6)
{
p2 = -1;
foundSites = CollectQuestSitesOfBuildingType(location, DFLocation.BuildingTypes.AnyHouse, p3);
}
// Must have found at least one site
if (foundSites == null || foundSites.Length == 0)
throw new Exception(string.Format("Could not find local site for {0} with P2={1} in {2}/{3}.", Symbol.Original, p2, location.RegionName, location.Name));
// Select a random site from available list
int selectedIndex = UnityEngine.Random.Range(0, foundSites.Length);
siteDetails = foundSites[selectedIndex];
}
#endregion
#region Remote Site Methods
/// <summary>
/// Get a remote site in the same region as player.
/// </summary>
void SetupRemoteSite(string line)
{
bool result = false;
switch(p1)
{
case 0:
result = SelectRemoteTownSite((DFLocation.BuildingTypes)p2);
break;
case 1:
result = SelectRemoteDungeonSite(p2);
break;
case 2:
result = SelectRemoteLocationExteriorSite(p2);
break;
default:
throw new Exception(string.Format("An unknown P1 value of {0} was encountered for Place {1}", p1, Symbol.Original));
}
// If searching for a dungeon and first numbered choice not found, then try again with any random dungeon type
if (!result && p1 == 1)
result = SelectRemoteDungeonSite(-1);
// Throw exception when remote place could not be selected, e.g. a dungeon of that type does not exist in this region
if (!result)
throw new Exception(string.Format("Search failed to locate matching remote site for Place {0} in region {1}. Resource source: '{2}'", Symbol.Original, GameManager.Instance.PlayerGPS.CurrentRegionName, line));
}
/// <summary>
/// Find a town for remote site containing building type.
/// Daggerfall's locations are so generic that we usually find a match within a few random attempts
/// compared to indexing several hundred locations and only selecting from known-good candidates.
/// In short, there are so many possible candidates it's not worth narrowing them down. Throw darts instead.
/// Basic checks are still done to reject unsuitable locations very quickly.
/// </summary>
bool SelectRemoteTownSite(DFLocation.BuildingTypes requiredBuildingType)
{
const int maxAttemptsBeforeFallback = 250;
const int maxAttemptsBeforeFailure = 500;
// Get player region
int regionIndex = GameManager.Instance.PlayerGPS.CurrentRegionIndex;
DFRegion regionData = DaggerfallUnity.Instance.ContentReader.MapFileReader.GetRegion(regionIndex);
int playerLocationIndex = GameManager.Instance.PlayerGPS.CurrentLocationIndex;
// Cannot use a region with no locations
// This should not happen in normal play
if (regionData.LocationCount == 0)
return false;
// Find random town containing building
int attempts = 0;
bool found = false;
while (!found)
{
// Increment attempts and do some fallback
if (++attempts >= maxAttemptsBeforeFallback &&
requiredBuildingType >= DFLocation.BuildingTypes.House1 &&
requiredBuildingType <= DFLocation.BuildingTypes.House6)
{
requiredBuildingType = DFLocation.BuildingTypes.AnyHouse;
p2 = -1;
}
if (attempts >= maxAttemptsBeforeFailure)
{
Debug.LogWarningFormat("Could not find remote town site with building type {0} within {1} attempts", requiredBuildingType.ToString(), attempts);
break;
}
// Get a random location index
int locationIndex = UnityEngine.Random.Range(0, (int)regionData.LocationCount);
// Discard the current player location if selected
if (locationIndex == playerLocationIndex)
continue;
// Discard all dungeon location types
if (IsDungeonType(regionData.MapTable[locationIndex].LocationType))
continue;
// Get location data for town
DFLocation location = DaggerfallUnity.Instance.ContentReader.MapFileReader.GetLocation(regionIndex, locationIndex);
if (!location.Loaded)
continue;
// Get list of valid sites
SiteDetails[] foundSites = null;
if (p2 == -1 && p3 == 0)
foundSites = CollectQuestSitesOfBuildingType(location, DFLocation.BuildingTypes.AllValid, p3);
else if (p2 == -1 && p3 == 1)
foundSites = CollectQuestSitesOfBuildingType(location, DFLocation.BuildingTypes.AnyHouse, p3);
else
{
// Check if town contains specified building type in MAPS.BSA directory
if (!HasBuildingType(location, requiredBuildingType))
continue;
// Get an array of potential quest sites with specified building type
// This ensures building site actually exists inside town, as MAPS.BSA directory can be incorrect
foundSites = CollectQuestSitesOfBuildingType(location, (DFLocation.BuildingTypes)p2, p3);
}
// Must have found at least one site
if (foundSites == null || foundSites.Length == 0)
continue;
// Select a random site from available list
int selectedIndex = UnityEngine.Random.Range(0, foundSites.Length);
siteDetails = foundSites[selectedIndex];
// All conditions have been satisfied
found = true;
}
//Debug.LogFormat("Found remote candidate site in {0} attempts", attempts);
return found;
}
/// <summary>
/// Find a random dungeon site in player region.
/// dungeonTypeIndex == -1 will select from all dungeons of type 0 through 16
/// dungeonTypeIndex == 0 through 16 will select from all available dungeons of that specific type
/// Note: Template only maps dungeon types 0-16 to p2 types dungeon0 through dungeon16.
/// This is probably because types 17-18 don't seem to contain quest markers.
/// Warning: Not all dungeon types are available in all regions. http://en.uesp.net/wiki/Daggerfall:Dungeons#Overview_of_Dungeon_Locations
/// </summary>
bool SelectRemoteDungeonSite(int dungeonTypeIndex)
{
// Get player region
int regionIndex = GameManager.Instance.PlayerGPS.CurrentRegionIndex;
DFRegion regionData = DaggerfallUnity.Instance.ContentReader.MapFileReader.GetRegion(regionIndex);
// Cannot use a region with no locations
// This should not happen in normal play
if (regionData.LocationCount == 0)
return false;
//Debug.LogFormat("Selecting for random dungeon of type {0} in {1}", dungeonTypeIndex, regionData.Name);
// Get indices for all dungeons of this type
int[] foundIndices = CollectDungeonIndicesOfType(regionData, dungeonTypeIndex);
if (foundIndices == null || foundIndices.Length == 0)
{
Debug.LogFormat("Could not find any random dungeons of type {0} in {1}", dungeonTypeIndex, regionData.Name);
return false;
}
//Debug.LogFormat("Found a total of {0} possible dungeons of type {1} in {2}", foundIndices.Length, dungeonTypeIndex, regionData.Name);
// Select a random dungeon location index from available list
int index = UnityEngine.Random.Range(0, foundIndices.Length);
// Get location data for selected dungeon
DFLocation location = DaggerfallUnity.Instance.ContentReader.MapFileReader.GetLocation(regionIndex, foundIndices[index]);
if (!location.Loaded)
return false;
// Dungeon must have at least one marker available
QuestMarker[] questSpawnMarkers, questItemMarkers;
EnumerateDungeonQuestMarkers(location, out questSpawnMarkers, out questItemMarkers);
if (!ValidateQuestMarkers(questSpawnMarkers, questItemMarkers))
{
Debug.LogFormat("Could not find any quest markers in random dungeon {0}", location.Name);
return false;
}
// Configure new site details
siteDetails = new SiteDetails();
siteDetails.questUID = ParentQuest.UID;
siteDetails.siteType = SiteTypes.Dungeon;
siteDetails.mapId = location.MapTableData.MapId;
siteDetails.locationId = location.Exterior.ExteriorData.LocationId;
siteDetails.regionIndex = location.RegionIndex;
siteDetails.regionName = location.RegionName;
siteDetails.locationName = location.Name;
siteDetails.questSpawnMarkers = questSpawnMarkers;
siteDetails.questItemMarkers = questItemMarkers;
return true;
}
/// <summary>
/// Find a random location exterior. This will create a SiteTypes.Town exterior site.
/// Location exteriors do not contain quest or item markers so cannot be used to "place item", "place foe", "place npc".
/// </summary>
/// <param name="locationTypeIndex">Location type index or -1.</param>
bool SelectRemoteLocationExteriorSite(int locationTypeIndex)
{
// Get player region
int regionIndex = GameManager.Instance.PlayerGPS.CurrentRegionIndex;
DFRegion regionData = DaggerfallUnity.Instance.ContentReader.MapFileReader.GetRegion(regionIndex);
// Cannot use a region with no locations
// This should not happen in normal play
if (regionData.LocationCount == 0)
return false;
// Get indices for all locations of this type
int[] foundIndices = CollectExteriorIndicesOfType(regionData, locationTypeIndex);
if (foundIndices == null || foundIndices.Length == 0)
{
Debug.LogFormat("Could not find any random location of index {0} in {1}", locationTypeIndex, regionData.Name);
return false;
}
// Select a random exterior location index from available list
int index = UnityEngine.Random.Range(0, foundIndices.Length);
// Get location data for selected exterior
DFLocation location = DaggerfallUnity.Instance.ContentReader.MapFileReader.GetLocation(regionIndex, foundIndices[index]);
if (!location.Loaded)
return false;
// Configure new site details
siteDetails = new SiteDetails();
siteDetails.questUID = ParentQuest.UID;
siteDetails.siteType = SiteTypes.Town;
siteDetails.mapId = location.MapTableData.MapId;
siteDetails.locationId = location.Exterior.ExteriorData.LocationId;
siteDetails.regionIndex = location.RegionIndex;
siteDetails.regionName = location.RegionName;
siteDetails.locationName = location.Name;
siteDetails.questSpawnMarkers = null;
siteDetails.questItemMarkers = null;
return true;
}
#endregion
#region Fixed Site Methods
/// <summary>
/// Setup a fixed location.
/// TODO: Not sure if need to support last byte of p2 for "sites transferred to by teleport cheat".
/// I *think* p2 just reference start markers inside dungeon but may have some placement meaning.
/// </summary>
void SetupFixedLocation()
{
// Attempt to get locationId by p1 - try p1 first then p1-1
// This should work out dungeon or exterior loctionId as needed
int buildingKey = 0;
SiteTypes siteType;
DFLocation location;
if (!DaggerfallUnity.Instance.ContentReader.GetQuestLocation(p1, out location))
{
// Could be a dungeon, attempt to get locationId by p1-1
if (!DaggerfallUnity.Instance.ContentReader.GetQuestLocation(p1 - 1, out location))
{
// p1 is a completely unknown locationId
throw new Exception(string.Format("Could not find locationId from p1 using: '{0}' or '{1}'", p1, p1 - 1));
}
else if (location.HasDungeon == true)
{
// If p1-1 resolves then dungeon is referenced
siteType = SiteTypes.Dungeon;
}
else
{
siteType = SiteTypes.Building;
}
}
else if (p1 == 50000)
{
// Hardcode ID for MantellanCrux as game data not a match
siteType = SiteTypes.Dungeon;
}
else
{
// If p1 does not resolve then exterior is referenced
siteType = SiteTypes.Town;
}
// Check for one or more quest markers inside dungeon
// Town sites do not have quest markers and are usually used only to reveal their location on travel map
QuestMarker[] questSpawnMarkers = null, questItemMarkers = null;