Unstable 0.17.4.0

This commit is contained in:
Markus Isberg
2022-03-30 00:08:09 +09:00
parent 2968e23ae8
commit c1b8e5a341
177 changed files with 3388 additions and 1977 deletions
@@ -520,8 +520,9 @@ namespace Barotrauma.MapCreatures.Behavior
if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; }
float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount;
if (branchHealAmount <= 0.0f) { continue; }
float prevHealth = branch.Health;
branch.Health += branchHealAmount;
branch.AccumulatedDamage -= branchHealAmount;
branch.AccumulatedDamage += (prevHealth - branch.Health);
}
}
StateMachine.Update(deltaTime);
@@ -633,7 +634,8 @@ namespace Barotrauma.MapCreatures.Behavior
{
if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f))
{
DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot);
float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth);
DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
}
if (branch.Health <= 0.0f)
{
@@ -836,7 +838,7 @@ namespace Barotrauma.MapCreatures.Behavior
}
#if SERVER
SendNetworkMessage(new BranchCreateEventData(newBranch, parent));
CreateNetworkMessage(new BranchCreateEventData(newBranch, parent));
#endif
return true;
}
@@ -878,7 +880,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!load)
{
SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
}
#endif
}
@@ -955,8 +957,6 @@ namespace Barotrauma.MapCreatures.Behavior
/// <param name="branch"></param>
private void CreateBody(BallastFloraBranch branch)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
Rectangle rect = branch.Rect;
Vector2 pos = Parent.Position + Offset + branch.Position;
@@ -975,6 +975,14 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
if (damage > 0)
{
damage = Math.Min(damage, branch.Health);
}
else
{
damage = Math.Max(damage, branch.Health - branch.MaxHealth);
}
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
@@ -983,8 +991,28 @@ namespace Barotrauma.MapCreatures.Behavior
if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; }
// damage is handled server side currently
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
if (GameMain.NetworkMember != null)
{
// damage is handled server side
if (GameMain.NetworkMember.IsClient)
{
return;
}
else
{
//accumulate damage on the server's side to ensure clients get notified
if (type == AttackType.Other || type == AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
}
}
}
if (attacker != null && toxinsCooldown <= 0)
{
@@ -1014,11 +1042,6 @@ namespace Barotrauma.MapCreatures.Behavior
}
branch.Health -= damage;
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
#if SERVER
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
@@ -1110,7 +1133,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!wasRemoved)
{
SendNetworkMessage(new BranchRemoveEventData(branch));
CreateNetworkMessage(new BranchRemoveEventData(branch));
}
#endif
}
@@ -1141,7 +1164,7 @@ namespace Barotrauma.MapCreatures.Behavior
}
});
#if SERVER
SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
#endif
}
@@ -1159,7 +1182,7 @@ namespace Barotrauma.MapCreatures.Behavior
StateMachine?.State?.Exit();
#if SERVER
SendNetworkMessage(new KillEventData());
CreateNetworkMessage(new KillEventData());
#endif
}
@@ -1181,7 +1204,7 @@ namespace Barotrauma.MapCreatures.Behavior
_entityList.Remove(this);
#if SERVER
SendNetworkMessage(new KillEventData());
CreateNetworkMessage(new KillEventData());
#endif
}
@@ -88,7 +88,7 @@ namespace Barotrauma
flash = element.GetAttributeBool("flash", showEffects);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow);
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
@@ -739,7 +739,7 @@ namespace Barotrauma
{
Rectangle rect = Rectangle.Empty;
if (element.Attribute("rect") != null)
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
@@ -747,15 +747,15 @@ namespace Barotrauma
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
int.Parse(element.GetAttribute("width").Value),
int.Parse(element.GetAttribute("height").Value));
}
bool isHorizontal = rect.Height > rect.Width;
var horizontalAttribute = element.Attribute("horizontal");
var horizontalAttribute = element.GetAttribute("horizontal");
if (horizontalAttribute != null)
{
isHorizontal = horizontalAttribute.Value.ToString() == "true";
@@ -1564,7 +1564,7 @@ namespace Barotrauma
public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect;
if (element.Attribute("rect") != null)
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
@@ -1572,10 +1572,10 @@ namespace Barotrauma
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
int.Parse(element.GetAttribute("width").Value),
int.Parse(element.GetAttribute("height").Value));
}
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element))
@@ -1639,7 +1639,7 @@ namespace Barotrauma
}
SerializableProperty.DeserializeProperties(hull, element);
if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
return hull;
}
@@ -160,17 +160,20 @@ namespace Barotrauma
public void Delete()
{
Dispose();
if (File.Exists(ContentFile.Path))
try
{
try
if (ContentPackage is { Files: { Length: 1 } }
&& ContentPackageManager.LocalPackages.Contains(ContentPackage))
{
File.Delete(ContentFile.Path);
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e);
Directory.Delete(ContentPackage.Dir, recursive: true);
ContentPackageManager.LocalPackages.Refresh();
ContentPackageManager.EnabledPackages.DisableRemovedMods();
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e);
}
}
public override void Dispose() { }
@@ -3826,12 +3826,12 @@ namespace Barotrauma
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost);
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
else
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost);
outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
foreach (string categoryToHide in locationType.HideEntitySubcategories)
@@ -31,8 +31,20 @@ namespace Barotrauma
public bool HasHuntingGrounds, OriginallyHadHuntingGrounds;
/// <summary>
/// Minimum difficulty of the level before hunting grounds can appear.
/// </summary>
public const float HuntingGroundsDifficultyThreshold = 25;
/// <summary>
/// Probability of hunting grounds appearing in 100% difficulty levels.
/// </summary>
public const float MaxHuntingGroundsProbability = 0.3f;
public OutpostGenerationParams ForceOutpostGenerationParams;
public bool AllowInvalidOutpost;
public readonly Point Size;
public readonly int InitialDepth;
@@ -150,11 +162,7 @@ namespace Barotrauma
}
else
{
//minimum difficulty of the level before hunting grounds can appear
float huntingGroundsDifficultyThreshold = 25;
//probability of hunting grounds appearing in 100% difficulty levels
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
}
IsBeaconActive = false;
@@ -235,7 +235,7 @@ namespace Barotrauma
UseNetworkSyncing = element.GetAttributeBool("networksyncing", false);
unrotatedForce =
element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ?
element.GetAttribute("force") != null && element.GetAttribute("force").Value.Contains(',') ?
element.GetAttributeVector2("force", Vector2.Zero) :
new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f);
File diff suppressed because it is too large Load Diff
@@ -182,8 +182,7 @@ namespace Barotrauma
{
for (int i = 0; i < Connections.Count; i++)
{
float maxHuntingGroundsProbability = 0.3f;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability;
connectionElements[i].SetAttributeValue("hashuntinggrounds", true);
}
}
@@ -232,7 +231,7 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
CurrentLocation.Discover(true);
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
InitProjectSpecific();
}
@@ -680,7 +679,7 @@ namespace Barotrauma
CurrentLocation.Discover();
SelectedLocation = null;
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
@@ -719,7 +718,7 @@ namespace Barotrauma
}
}
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
@@ -857,16 +856,14 @@ namespace Barotrauma
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
ProgressLocationTypeChanges(location);
if (location.Discovered)
if (!ProgressLocationTypeChanges(location) && location.Discovered)
{
location.UpdateStore();
location.UpdateStores();
}
}
}
private void ProgressLocationTypeChanges(Location location)
private bool ProgressLocationTypeChanges(Location location)
{
location.TimeSinceLastTypeChange++;
location.LocationTypeChangeCooldown--;
@@ -886,9 +883,8 @@ namespace Barotrauma
location.PendingLocationTypeChange.Value.parentMission);
if (location.PendingLocationTypeChange.Value.delay <= 0)
{
ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
}
return;
}
}
@@ -920,9 +916,9 @@ namespace Barotrauma
}
else
{
ChangeLocationType(location, selectedTypeChange);
return ChangeLocationType(location, selectedTypeChange);
}
return;
return false;
}
}
@@ -941,6 +937,8 @@ namespace Barotrauma
}
}
}
return false;
}
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
@@ -988,7 +986,7 @@ namespace Barotrauma
return distance;
}
private void ChangeLocationType(Location location, LocationTypeChange change)
private bool ChangeLocationType(Location location, LocationTypeChange change)
{
string prevName = location.Name;
@@ -996,7 +994,7 @@ namespace Barotrauma
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
return;
return false;
}
if (newType.OutpostTeam != location.Type.OutpostTeam ||
@@ -1013,6 +1011,7 @@ namespace Barotrauma
location.TimeSinceLastTypeChange = 0;
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
location.PendingLocationTypeChange = null;
return true;
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
@@ -1091,7 +1090,7 @@ namespace Barotrauma
}
}
location.LoadStore(subElement);
location.LoadStores(subElement);
location.LoadMissions(subElement);
break;
@@ -59,8 +59,19 @@ namespace Barotrauma
}
public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32));
private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private static string RemoveWhitespace(string s)
{
StringBuilder sb = new StringBuilder(s.Length / 2); // Reserve half the size of the original string because
// that's probably close enough to the size of the result
for (int i = 0; i < s.Length; i++)
{
if (char.IsWhiteSpace(s[i])) { continue; }
sb.Append(s[i]);
}
return sb.ToString();
}
//thanks to Jlobblet for this regex
private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
@@ -175,7 +186,7 @@ namespace Barotrauma
}
if (options.HasFlag(StringHashOptions.IgnoreWhitespace))
{
str = removeWhitespaceRegex.Replace(str, "");
str = RemoveWhitespace(str);
}
byte[] bytes = Encoding.UTF8.GetBytes(str);
return CalculateForBytes(bytes);
@@ -156,6 +156,8 @@ namespace Barotrauma
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
private ImmutableHashSet<Identifier> StoreIdentifiers { get; set; }
#warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes
public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
@@ -230,6 +232,26 @@ namespace Barotrauma
return humanPrefabCollections.GetRandom(randSync);
}
public ImmutableHashSet<Identifier> GetStoreIdentifiers()
{
if (StoreIdentifiers == null)
{
var storeIdentifiers = new HashSet<Identifier>();
foreach (var collection in humanPrefabCollections)
{
foreach (var prefab in collection)
{
if (prefab?.CampaignInteractionType == CampaignMode.InteractionType.Store)
{
storeIdentifiers.Add(prefab.Identifier);
}
}
}
StoreIdentifiers = storeIdentifiers.ToImmutableHashSet();
}
return StoreIdentifiers;
}
public override void Dispose() { }
}
}
@@ -57,17 +57,17 @@ namespace Barotrauma
}
}
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false)
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, locationType, location: null, onlyEntrance);
return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost);
}
public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false)
public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, location.Type, location, onlyEntrance);
return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost);
}
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false)
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostModuleFile>())
@@ -197,12 +197,19 @@ namespace Barotrauma
AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams);
if (pendingModuleFlags.Any(flag => flag != "none"))
{
remainingTries--;
if (remainingTries <= 0)
if (!allowInvalidOutpost)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
remainingTries--;
if (remainingTries <= 0)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
}
continue;
}
else
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
}
continue;
}
var outpostInfo = new SubmarineInfo()
@@ -328,7 +335,10 @@ namespace Barotrauma
selectedModule.Offset =
(selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) -
selectedModule.ThisGap.WorldPosition;
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
{
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
}
}
entities[selectedModule] = moduleEntities;
}
@@ -464,6 +474,16 @@ namespace Barotrauma
pendingModuleFlags.Remove(initialModuleFlag);
pendingModuleFlags.Insert(0, initialModuleFlag);
if (pendingModuleFlags.Count > totalModuleCount)
{
DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules...");
int removeCount = pendingModuleFlags.Count - totalModuleCount;
for (int i = 0; i < removeCount; i++)
{
pendingModuleFlags.Remove(pendingModuleFlags.Last());
}
}
return pendingModuleFlags;
}
@@ -486,46 +506,71 @@ namespace Barotrauma
if (pendingModuleFlags.Count == 0) { return true; }
List<PlacedModule> placedModules = new List<PlacedModule>();
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
for (int i = 0; i < 2; i++)
{
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
//try placing a module meant for this location type first, and if that fails, try choosing whatever fits
bool allowDifferentLocationType = i > 0;
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType);
if (newModule != null) { placedModules.Add(newModule); }
if (pendingModuleFlags.Count == 0) { return true; }
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
PlacedModule newModule = null;
//try appending to the current module if possible
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
}
if (newModule != null)
{
placedModules.Add(newModule);
}
else
{
//couldn't append to current module, try one of the other placed modules
foreach (PlacedModule otherModule in selectedModules)
{
if (otherModule == currentModule) { continue; }
foreach (OutpostModuleInfo.GapPosition otherGapPosition in
GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
{
newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
if (newModule != null)
{
placedModules.Add(newModule);
break;
}
}
if (newModule != null) { break; }
}
}
if (pendingModuleFlags.Count == 0) { return true; }
}
}
//couldn't place anything, retry
if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule))
//couldn't place a module anywhere, we're probably fucked!
if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule))
{
//try to append to some other module first
foreach (PlacedModule otherModule in selectedModules)
{
if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
return true;
}
}
//try to replace the previously placed module with something else that we can append to
var failedModule = currentModule;
for (int i = 0; i < 10; i++)
{
selectedModules.Remove(currentModule);
assertAllPreviousModulesPresent();
//readd the module types that the previous module was supposed to fulfill to the pending module types
pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes);
if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); }
//retry
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType);
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true);
assertAllPreviousModulesPresent();
if (currentModule == null) { break; }
if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
assertAllPreviousModulesPresent();
return true;
}
}
@@ -534,9 +579,14 @@ namespace Barotrauma
foreach (PlacedModule placedModule in placedModules)
{
AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType);
AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule);
}
return placedModules.Count > 0;
void assertAllPreviousModulesPresent()
{
System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
}
}
/// <summary>
@@ -553,7 +603,8 @@ namespace Barotrauma
List<SubmarineInfo> availableModules,
List<Identifier> pendingModuleFlags,
List<PlacedModule> selectedModules,
LocationType locationType)
LocationType locationType,
bool allowDifferentLocationType)
{
if (pendingModuleFlags.Count == 0) { return null; }
@@ -562,7 +613,7 @@ namespace Barotrauma
foreach (Identifier moduleFlag in pendingModuleFlags)
{
flagToPlace = moduleFlag;
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType);
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType);
if (nextModule != null) { break; }
}
@@ -603,6 +654,7 @@ namespace Barotrauma
foreach (PlacedModule otherModule in modules2)
{
if (module == otherModule) { continue; }
if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; }
if (ModulesOverlap(module, otherModule))
{
module1 = module;
@@ -775,22 +827,25 @@ namespace Barotrauma
}
}
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType)
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType)
{
IEnumerable<SubmarineInfo> availableModules = null;
if (moduleFlag.IsEmpty || moduleFlag.Equals("none"))
{
availableModules = modules
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())));
}
else
{
availableModules = modules
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
}
availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
if (prevModule != null)
{
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo));
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo));
}
if (availableModules.Count() == 0) { return null; }
@@ -800,15 +855,22 @@ namespace Barotrauma
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
if (allowDifferentLocationType && !modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
{
DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
if (allowDifferentLocationType)
{
DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
else
{
return null;
}
}
else
{
@@ -1039,16 +1101,20 @@ namespace Barotrauma
if (hallwayLength <= 1.0f) { continue; }
var suitableModules = availableModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableModules.Count() == 0)
Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier();
var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
var suitableHallwayModules = hallwayModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableHallwayModules.Count() == 0)
{
suitableModules = availableModules.Where(m =>
suitableHallwayModules = hallwayModules.Where(m =>
!m.OutpostModuleInfo.AllowAttachToModules.Any() ||
m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any"));
}
var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType);
var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType);
if (hallwayInfo == null)
{
DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\".");
@@ -1170,7 +1236,7 @@ namespace Barotrauma
var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap);
if (startWaypoint == null)
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
continue;
}
var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap);
@@ -45,6 +45,9 @@ namespace Barotrauma
[Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")]
public GapPosition GapPositions { get; set; }
[Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")]
public GapPosition CanAttachToPrevious { get; set; }
public string Name { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
@@ -6,30 +6,31 @@ namespace Barotrauma
{
class PriceInfo
{
public readonly int Price;
public readonly bool CanBeBought;
public int Price { get; }
public bool CanBeBought { get; }
//minimum number of items available at a given store
public readonly int MinAvailableAmount;
public int MinAvailableAmount { get; }
//maximum number of items available at a given store
public readonly int MaxAvailableAmount;
public int MaxAvailableAmount { get; }
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public bool CanBeSpecial { get; }
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public int MinLevelDifficulty { get; }
/// <summary>
/// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store.
/// </summary>
public float BuyingPriceMultiplier { get; } = 1f;
public bool DisplayNonEmpty { get; } = false;
public Identifier StoreIdentifier { get; }
/// <summary>
/// Used when both <see cref="MinAvailableAmount"/> and <see cref="MaxAvailableAmount"/> are set to 0.
/// </summary>
public const int DefaultAmount = 5;
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public readonly bool CanBeSpecial;
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public readonly int MinLevelDifficulty;
/// <summary>
/// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store.
/// </summary>
public readonly float BuyingPriceMultiplier = 1f;
public bool DisplayNonEmpty { get; } = false;
/// <summary>
/// Support for the old style of determining item prices
@@ -42,14 +43,16 @@ namespace Barotrauma
MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
CanBeBought = true;
var minAmount = GetMinAmount(element);
int minAmount = GetMinAmount(element);
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
var maxAmount = GetMaxAmount(element);
int maxAmount = GetMaxAmount(element);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount);
}
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, bool displayNonEmpty = false)
public PriceInfo(int price, bool canBeBought,
int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f,
bool displayNonEmpty = false, string storeIdentifier = null)
{
Price = price;
CanBeBought = canBeBought;
@@ -60,48 +63,67 @@ namespace Barotrauma
MinLevelDifficulty = minLevelDifficulty;
CanBeSpecial = canBeSpecial;
DisplayNonEmpty = displayNonEmpty;
StoreIdentifier = new Identifier(storeIdentifier);
}
public static List<Tuple<Identifier, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
public static List<PriceInfo> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
{
var priceInfos = new List<PriceInfo>();
defaultPrice = null;
int basePrice = element.GetAttributeInt("baseprice", 0);
bool soldByDefault = element.GetAttributeBool("soldbydefault", true);
int minAmount = GetMinAmount(element);
int maxAmount = GetMaxAmount(element);
int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
bool canBeSpecial = element.GetAttributeBool("canbespecial", true);
float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
var priceInfos = new List<Tuple<Identifier, PriceInfo>>();
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
foreach (XElement childElement in element.GetChildElements("price"))
{
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
priceInfos.Add(new Tuple<Identifier, PriceInfo>(childElement.GetAttributeIdentifier("locationtype", ""),
new PriceInfo((int)(priceMultiplier * basePrice), sold,
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty);
float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier);
string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", "");
if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier))
{
backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}";
}
string[] storeIdentifiers = childElement.GetAttributeStringArray("storeidentifiers", new string[1] { backwardsCompatibleIdentifier });
foreach (string id in storeIdentifiers)
{
if (string.IsNullOrEmpty(id)) { continue; }
// TODO: Add some error messages if we have defined the min or max amount while the item is not sold
var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice),
sold,
sold ? GetMinAmount(childElement, minAmount) : 0,
sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial,
childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty),
childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier),
displayNonEmpty)));
storeMinLevelDifficulty,
storeBuyingMultiplier,
displayNonEmpty,
id);
priceInfos.Add(priceInfo);
}
}
bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
canBeBoughtAtOtherLocations ? minAmount : 0,
canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty);
bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false));
defaultPrice = new PriceInfo(basePrice,
soldElsewhere,
soldElsewhere ? minAmount : 0,
soldElsewhere ? maxAmount : 0,
canBeSpecial,
minLevelDifficulty,
buyingPriceMultiplier,
displayNonEmpty);
return priceInfos;
}
private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ?
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue;
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) :
defaultValue;
private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ?
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue;
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) :
defaultValue;
}
}
@@ -48,11 +48,11 @@ namespace Barotrauma
{
if (!subs.Any()) yield return CoroutineStatus.Success;
Character.Controlled = null;
cam.TargetPos = Vector2.Zero;
#if CLIENT
Character.Controlled = null;
GameMain.LightManager.LosEnabled = false;
#endif
cam.TargetPos = Vector2.Zero;
Level.Loaded.TopBarrier.Enabled = false;
@@ -1374,7 +1374,7 @@ namespace Barotrauma
public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
string name = element.Attribute("name").Value;
string name = element.GetAttribute("name").Value;
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = FindPrefab(name, identifier);
@@ -1440,12 +1440,12 @@ namespace Barotrauma
if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); }
//structures with a body drop a shadow by default
if (element.Attribute("usedropshadow") == null)
if (element.GetAttribute("usedropshadow") == null)
{
s.UseDropShadow = prefab.Body;
}
if (element.Attribute("noaitarget") == null)
if (element.GetAttribute("noaitarget") == null)
{
s.NoAITarget = prefab.NoAITarget;
}
@@ -162,7 +162,7 @@ namespace Barotrauma
tags.Add(tag.Trim().ToIdentifier());
}
if (element.Attribute("ishorizontal") != null)
if (element.GetAttribute("ishorizontal") != null)
{
IsHorizontal = element.GetAttributeBool("ishorizontal", false);
}
@@ -177,8 +177,8 @@ namespace Barotrauma
{
case "sprite":
Sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("sheetindex") == null)
if (subElement.GetAttribute("sourcerect") == null &&
subElement.GetAttribute("sheetindex") == null)
{
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!");
}
@@ -191,7 +191,7 @@ namespace Barotrauma
CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace())
if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace())
{
Sprite.Name = Name.Value;
}
@@ -199,7 +199,7 @@ namespace Barotrauma
break;
case "backgroundsprite":
BackgroundSprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null && Sprite != null)
if (subElement.GetAttribute("sourcerect") == null && Sprite != null)
{
BackgroundSprite.SourceRect = Sprite.SourceRect;
BackgroundSprite.size = Sprite.size;
@@ -223,7 +223,7 @@ namespace Barotrauma
int groupID = 0;
DecorativeSprite decorativeSprite = null;
if (subElement.Attribute("texture") == null)
if (subElement.GetAttribute("texture") == null)
{
groupID = subElement.GetAttributeInt("randomgroupid", 0);
}
@@ -284,10 +284,10 @@ namespace Barotrauma
}
//backwards compatibility
if (element.Attribute("size") == null)
if (element.GetAttribute("size") == null)
{
Size = Vector2.Zero;
if (element.Attribute("width") == null && element.Attribute("height") == null)
if (element.GetAttribute("width") == null && element.GetAttribute("height") == null)
{
Size = Sprite.SourceRect.Size.ToVector2();
}
@@ -322,7 +322,7 @@ namespace Barotrauma
#endif
Tags = tags.ToImmutableHashSet();
AllowedLinks = Enumerable.Empty<Identifier>().ToImmutableHashSet();
AllowedLinks = ImmutableHashSet<Identifier>.Empty;
}
protected override void CreateInstance(Rectangle rect)
@@ -253,7 +253,7 @@ namespace Barotrauma
get { return subBody == null ? Vector2.Zero : subBody.Velocity; }
set
{
if (subBody == null) return;
if (subBody == null) { return; }
subBody.Velocity = value;
}
}
@@ -969,8 +969,6 @@ namespace Barotrauma
public void Update(float deltaTime)
{
//if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX();
if (Info.IsWreck)
{
WreckAI?.Update(deltaTime);
@@ -266,7 +266,7 @@ namespace Barotrauma
GameVersion = original.GameVersion;
Type = original.Type;
SubmarineClass = original.SubmarineClass;
hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null;
hash = !string.IsNullOrEmpty(original.FilePath) && File.Exists(original.FilePath) ? original.MD5Hash : null;
Dimensions = original.Dimensions;
CargoCapacity = original.CargoCapacity;
FilePath = original.FilePath;
@@ -299,7 +299,7 @@ namespace Barotrauma
DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms...");
Thread.Sleep(250);
}
if (doc == null || doc.Root == null)
if (doc?.Root == null)
{
IsFileCorrupted = true;
return;
@@ -987,8 +987,8 @@ namespace Barotrauma
public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
(int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
@@ -1024,9 +1024,9 @@ namespace Barotrauma
w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0));
int i = 0;
while (element.Attribute("linkedto" + i) != null)
while (element.GetAttribute("linkedto" + i) != null)
{
int srcId = int.Parse(element.Attribute("linkedto" + i).Value);
int srcId = int.Parse(element.GetAttribute("linkedto" + i).Value);
int destId = idRemap.GetOffsetId(srcId);
if (destId > 0)
{