Unstable 0.17.5.0
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<Preset
|
||||
name="Moderator"
|
||||
description="Allowed to manage round settings, kick players and access server logs."
|
||||
permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog">
|
||||
permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney">
|
||||
<Command name="clientlist"/>
|
||||
<Command name="readycheck"/>
|
||||
<Command name="autorestart"/>
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private Wallet wallet = new Wallet();
|
||||
private Wallet wallet;
|
||||
|
||||
public Wallet Wallet
|
||||
{
|
||||
@@ -1055,8 +1055,13 @@ namespace Barotrauma
|
||||
return newCharacter;
|
||||
}
|
||||
|
||||
private Character(Submarine submarine, ushort id): base(submarine, id)
|
||||
{
|
||||
wallet = new Wallet(Option<Character>.Some(this));
|
||||
}
|
||||
|
||||
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
|
||||
: base(null, id)
|
||||
: this(null, id)
|
||||
{
|
||||
this.Seed = seed;
|
||||
this.Prefab = prefab;
|
||||
|
||||
+15
-5
@@ -172,11 +172,21 @@ namespace Barotrauma
|
||||
SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ??
|
||||
ContentPackageManager.CorePackages.First());
|
||||
}
|
||||
SetRegular(Regular
|
||||
.Select(p => ContentPackageManager.RegularPackages.Contains(p)
|
||||
? p
|
||||
: ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 => p2.SteamWorkshopId == p.SteamWorkshopId))
|
||||
.ToArray());
|
||||
|
||||
List<RegularPackage> newRegular = new List<RegularPackage>();
|
||||
foreach (var p in Regular)
|
||||
{
|
||||
if (ContentPackageManager.RegularPackages.Contains(p))
|
||||
{
|
||||
newRegular.Add(p);
|
||||
}
|
||||
else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2
|
||||
=> p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP)
|
||||
{
|
||||
newRegular.Add(newP);
|
||||
}
|
||||
}
|
||||
SetRegular(newRegular);
|
||||
}
|
||||
|
||||
public static void BackUp()
|
||||
|
||||
@@ -115,6 +115,24 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public void Remove() => Element.Remove();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ContentXElement element && this == element;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(ContentPackage, Element);
|
||||
}
|
||||
|
||||
public static bool operator ==(in ContentXElement? a, in ContentXElement? b)
|
||||
{
|
||||
return a?.ContentPackage == b?.ContentPackage && a?.Element == b?.Element;
|
||||
}
|
||||
|
||||
public static bool operator !=(in ContentXElement? a, in ContentXElement? b) =>
|
||||
!(a == b);
|
||||
}
|
||||
|
||||
public static class ContentXElementExtensions
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Barotrauma
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum TransitionMode
|
||||
{
|
||||
@@ -146,4 +148,11 @@
|
||||
AlwaysStayConscious,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CharacterType
|
||||
{
|
||||
Bot = 0b01,
|
||||
Player = 0b10,
|
||||
Both = Bot | Player
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ namespace Barotrauma
|
||||
float difficultyMultiplier = 1 + level.Difficulty / 100f;
|
||||
baseExperienceGain *= difficultyMultiplier;
|
||||
|
||||
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters();
|
||||
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
|
||||
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
|
||||
@@ -380,7 +380,7 @@ namespace Barotrauma
|
||||
GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value);
|
||||
|
||||
#if SERVER
|
||||
totalReward = DistributeRewardsToCrew(GetSalaryEligibleCrew(), totalReward);
|
||||
totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward);
|
||||
#endif
|
||||
if (totalReward > 0)
|
||||
{
|
||||
@@ -436,18 +436,6 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
public static IEnumerable<Character> GetSalaryEligibleCrew()
|
||||
{
|
||||
if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return Array.Empty<Character>(); }
|
||||
|
||||
IEnumerable<Character> characters = crewManager.GetCharacters();
|
||||
#if SERVER
|
||||
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters);
|
||||
#elif CLIENT
|
||||
return characters;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static int GetRewardDistibutionSum(IEnumerable<Character> crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution;
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Barotrauma
|
||||
|
||||
public readonly bool IsSideObjective;
|
||||
|
||||
public readonly bool RequireWreck;
|
||||
public readonly bool RequireWreck, RequireRuin;
|
||||
|
||||
/// <summary>
|
||||
/// The mission can only be received when travelling from a location of the first type to a location of the second type
|
||||
@@ -152,6 +152,7 @@ namespace Barotrauma
|
||||
AllowRetry = element.GetAttributeBool("allowretry", false);
|
||||
IsSideObjective = element.GetAttributeBool("sideobjective", false);
|
||||
RequireWreck = element.GetAttributeBool("requirewreck", false);
|
||||
RequireRuin = element.GetAttributeBool("requireruin", false);
|
||||
Commonness = element.GetAttributeInt("commonness", 1);
|
||||
if (element.GetAttribute("difficulty") != null)
|
||||
{
|
||||
|
||||
@@ -226,6 +226,7 @@ namespace Barotrauma
|
||||
|
||||
public void SetPurchasedItems(Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
|
||||
{
|
||||
if (purchasedItems.Count == 0 && PurchasedItems.Count == 0) { return; }
|
||||
PurchasedItems.Clear();
|
||||
foreach (var entry in purchasedItems)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Barotrauma
|
||||
if (reputationChange > 0f)
|
||||
{
|
||||
float reputationGainMultiplier = 1f;
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters())
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
internal readonly struct WalletChangedEvent
|
||||
{
|
||||
public readonly Option<Character> Owner;
|
||||
public readonly Wallet Wallet;
|
||||
public readonly WalletInfo Info;
|
||||
public readonly WalletChangedData ChangedData;
|
||||
@@ -15,6 +16,7 @@ namespace Barotrauma
|
||||
Wallet = wallet;
|
||||
Info = info;
|
||||
ChangedData = changedData;
|
||||
Owner = wallet.Owner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +111,8 @@ namespace Barotrauma
|
||||
// ReSharper disable ValueParameterNotUsed
|
||||
internal sealed class InvalidWallet : Wallet
|
||||
{
|
||||
public InvalidWallet(): base(Option<Character>.None()) { }
|
||||
|
||||
public override int Balance
|
||||
{
|
||||
get => 0;
|
||||
@@ -132,6 +136,8 @@ namespace Barotrauma
|
||||
AttrubuteNameRewardDistribution = "rewarddistribution",
|
||||
SaveElementName = "Wallet";
|
||||
|
||||
public readonly Option<Character> Owner;
|
||||
|
||||
private int balance;
|
||||
|
||||
public virtual int Balance
|
||||
@@ -148,9 +154,12 @@ namespace Barotrauma
|
||||
set => rewardDistribution = ClampRewardDistribution(value);
|
||||
}
|
||||
|
||||
public Wallet() { }
|
||||
public Wallet(Option<Character> owner)
|
||||
{
|
||||
Owner = owner;
|
||||
}
|
||||
|
||||
public Wallet(XElement element)
|
||||
public Wallet(Option<Character> owner, XElement element): this(owner)
|
||||
{
|
||||
balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0));
|
||||
rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0));
|
||||
@@ -185,7 +194,7 @@ namespace Barotrauma
|
||||
SettingsChanged(balanceChanged: Option<int>.Some(-price), rewardChanged: Option<int>.None());
|
||||
}
|
||||
|
||||
public void SetRewardDistrubiton(int value)
|
||||
public void SetRewardDistribution(int value)
|
||||
{
|
||||
int oldValue = RewardDistribution;
|
||||
RewardDistribution = value;
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Barotrauma
|
||||
public int GetAddedMissionCount()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters())
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
|
||||
}
|
||||
@@ -68,6 +68,15 @@ namespace Barotrauma
|
||||
|
||||
abstract partial class CampaignMode : GameMode
|
||||
{
|
||||
[NetworkSerialize]
|
||||
public struct SaveInfo : INetSerializableStruct
|
||||
{
|
||||
public string FilePath;
|
||||
public int SaveTime;
|
||||
public string SubmarineName;
|
||||
public string[] EnabledContentPackageNames;
|
||||
}
|
||||
|
||||
public const int MaxMoney = int.MaxValue / 2; //about 1 billion
|
||||
public const int InitialMoney = 8500;
|
||||
|
||||
@@ -180,13 +189,37 @@ namespace Barotrauma
|
||||
protected CampaignMode(GameModePreset preset)
|
||||
: base(preset)
|
||||
{
|
||||
Bank = new Wallet
|
||||
Bank = new Wallet(Option<Character>.None())
|
||||
{
|
||||
Balance = InitialMoney
|
||||
};
|
||||
|
||||
CargoManager = new CargoManager(this);
|
||||
MedicalClinic = new MedicalClinic(this);
|
||||
Identifier messageIdentifier = new Identifier("money");
|
||||
|
||||
#if CLIENT
|
||||
OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e =>
|
||||
{
|
||||
if (!(e.ChangedData.BalanceChanged is Some<int> { Value: var changed })) { return; }
|
||||
|
||||
bool isGain = changed > 0;
|
||||
|
||||
Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red;
|
||||
|
||||
switch (e.Owner)
|
||||
{
|
||||
case Some<Character> { Value: var owner}:
|
||||
owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed);
|
||||
break;
|
||||
case None<Character> _ when IsSinglePlayer:
|
||||
Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed);
|
||||
break;
|
||||
}
|
||||
|
||||
string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
public virtual Wallet GetWallet(Client client = null)
|
||||
|
||||
+2
-2
@@ -167,7 +167,7 @@ namespace Barotrauma
|
||||
LoadStats(subElement);
|
||||
break;
|
||||
case Wallet.LowerCaseSaveElementName:
|
||||
Bank = new Wallet(subElement);
|
||||
Bank = new Wallet(Option<Character>.None(), subElement);
|
||||
break;
|
||||
#if SERVER
|
||||
case "savedexperiencepoints":
|
||||
@@ -183,7 +183,7 @@ namespace Barotrauma
|
||||
int oldMoney = element.GetAttributeInt("money", 0);
|
||||
if (oldMoney > 0)
|
||||
{
|
||||
Bank = new Wallet
|
||||
Bank = new Wallet(Option<Character>.None())
|
||||
{
|
||||
Balance = oldMoney
|
||||
};
|
||||
|
||||
@@ -735,14 +735,40 @@ namespace Barotrauma
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public static IEnumerable<Character> GetSessionCrewCharacters()
|
||||
/// <summary>
|
||||
/// Returns a list of crew characters currently in the game with a given filter.
|
||||
/// </summary>
|
||||
/// <param name="type">Character type filter</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// In singleplayer mode the CharacterType.Player returns the currently controlled player.
|
||||
/// </remarks>
|
||||
public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
|
||||
{
|
||||
if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet<Character>.Empty; }
|
||||
|
||||
IEnumerable<Character> players;
|
||||
IEnumerable<Character> bots;
|
||||
HashSet<Character> characters = new HashSet<Character>();
|
||||
|
||||
#if SERVER
|
||||
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
|
||||
#else
|
||||
if (GameMain.GameSession?.CrewManager is null) { return Enumerable.Empty<Character>(); }
|
||||
return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null && !c.IsDead);
|
||||
players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
|
||||
bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
|
||||
#elif CLIENT
|
||||
players = crewManager.GetCharacters().Where(c => c.IsPlayer);
|
||||
bots = crewManager.GetCharacters().Where(c => c.IsBot);
|
||||
#endif
|
||||
if (type.HasFlag(CharacterType.Bot))
|
||||
{
|
||||
foreach (Character bot in bots) { characters.Add(bot); }
|
||||
}
|
||||
|
||||
if (type.HasFlag(CharacterType.Player))
|
||||
{
|
||||
foreach (Character player in players) { characters.Add(player); }
|
||||
}
|
||||
|
||||
return characters.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
public void EndRound(string endMessage, List<TraitorMissionResult>? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
|
||||
@@ -754,7 +780,7 @@ namespace Barotrauma
|
||||
|
||||
try
|
||||
{
|
||||
ImmutableArray<Character> crewCharacters = GetSessionCrewCharacters().ToImmutableArray();
|
||||
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
int prevMoney = GetAmountOfMoney(crewCharacters);
|
||||
|
||||
@@ -898,7 +924,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Character c in GetSessionCrewCharacters())
|
||||
foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
foreach (var itemSelectedDuration in c.ItemSelectedDurations)
|
||||
{
|
||||
@@ -948,25 +974,25 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackagePaths, out LocalizedString errorMsg)
|
||||
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
|
||||
{
|
||||
errorMsg = "";
|
||||
//no known content packages, must be an older save file
|
||||
if (!contentPackagePaths.Any()) { return true; }
|
||||
if (!contentPackageNames.Any()) { return true; }
|
||||
|
||||
List<string> missingPackages = new List<string>();
|
||||
foreach (string packagePath in contentPackagePaths)
|
||||
foreach (string packageName in contentPackageNames)
|
||||
{
|
||||
if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.Path == packagePath))
|
||||
if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName)))
|
||||
{
|
||||
missingPackages.Add(packagePath);
|
||||
missingPackages.Add(packageName);
|
||||
}
|
||||
}
|
||||
List<string> excessPackages = new List<string>();
|
||||
foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All)
|
||||
{
|
||||
if (!cp.HasMultiplayerSyncedContent) { continue; }
|
||||
if (!contentPackagePaths.Any(p => p == cp.Path))
|
||||
if (!contentPackageNames.Any(p => cp.NameMatches(p)))
|
||||
{
|
||||
excessPackages.Add(cp.Name);
|
||||
}
|
||||
@@ -976,9 +1002,9 @@ namespace Barotrauma
|
||||
if (missingPackages.Count == 0 && missingPackages.Count == 0)
|
||||
{
|
||||
var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray();
|
||||
for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++)
|
||||
for (int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++)
|
||||
{
|
||||
if (contentPackagePaths[i] != enabledPackages[i].Path)
|
||||
if (!enabledPackages[i].NameMatches(contentPackageNames[i]))
|
||||
{
|
||||
orderMismatch = true;
|
||||
break;
|
||||
@@ -1009,7 +1035,7 @@ namespace Barotrauma
|
||||
if (orderMismatch)
|
||||
{
|
||||
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
|
||||
errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackagePaths));
|
||||
errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackageNames));
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1040,8 +1066,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
|
||||
rootElement.Add(new XAttribute("selectedcontentpackages",
|
||||
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Path))));
|
||||
rootElement.Add(new XAttribute("selectedcontentpackagenames",
|
||||
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|")))));
|
||||
|
||||
((CampaignMode)GameMode).Save(doc.Root);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -13,6 +12,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
[Editable]
|
||||
public LimbType LimbType { get; set; }
|
||||
|
||||
[Editable]
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components
|
||||
partial class Controller : ItemComponent, IServerSerializable
|
||||
{
|
||||
//where the limbs of the user should be positioned when using the controller
|
||||
private readonly List<LimbPos> limbPositions;
|
||||
private readonly List<LimbPos> limbPositions = new List<LimbPos>();
|
||||
|
||||
private Direction dir;
|
||||
|
||||
@@ -117,38 +117,9 @@ namespace Barotrauma.Items.Components
|
||||
public Controller(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
limbPositions = new List<LimbPos>();
|
||||
|
||||
userPos = element.GetAttributeVector2("UserPos", Vector2.Zero);
|
||||
|
||||
Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name != "limbposition") { continue; }
|
||||
string limbStr = subElement.GetAttributeString("limb", "");
|
||||
if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.");
|
||||
}
|
||||
else
|
||||
{
|
||||
LimbPos limbPos = new LimbPos(limbType,
|
||||
subElement.GetAttributeVector2("position", Vector2.Zero),
|
||||
subElement.GetAttributeBool("allowusinglimb", false));
|
||||
limbPositions.Add(limbPos);
|
||||
if (!limbPos.AllowUsingLimb)
|
||||
{
|
||||
if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm ||
|
||||
limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm)
|
||||
{
|
||||
AllowAiming = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LoadLimbPositions(element);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -529,5 +500,63 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
partial void HideHUDs(bool value);
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
return SaveLimbPositions(base.Save(parentElement));
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode)
|
||||
{
|
||||
LoadLimbPositions(componentElement);
|
||||
}
|
||||
}
|
||||
|
||||
private XElement SaveLimbPositions(XElement element)
|
||||
{
|
||||
if (Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
foreach (var limbPos in limbPositions)
|
||||
{
|
||||
element.Add(new XElement("limbposition",
|
||||
new XAttribute("limb", limbPos.LimbType),
|
||||
new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)),
|
||||
new XAttribute("allowusinglimb", limbPos.AllowUsingLimb)));
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
private void LoadLimbPositions(XElement element)
|
||||
{
|
||||
limbPositions.Clear();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name != "limbposition") { continue; }
|
||||
string limbStr = subElement.GetAttributeString("limb", "");
|
||||
if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.");
|
||||
}
|
||||
else
|
||||
{
|
||||
LimbPos limbPos = new LimbPos(limbType,
|
||||
subElement.GetAttributeVector2("position", Vector2.Zero),
|
||||
subElement.GetAttributeBool("allowusinglimb", false));
|
||||
limbPositions.Add(limbPos);
|
||||
if (!limbPos.AllowUsingLimb)
|
||||
{
|
||||
if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm ||
|
||||
limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm)
|
||||
{
|
||||
AllowAiming = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float progressState;
|
||||
|
||||
private readonly Dictionary<uint, int> fabricationLimits = new Dictionary<uint, int>();
|
||||
|
||||
public Fabricator(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -105,6 +107,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
fabricationRecipes.Add(recipe.RecipeHash, recipe);
|
||||
if (recipe.FabricationLimitMax >= 0)
|
||||
{
|
||||
fabricationLimits.Add(recipe.RecipeHash, Rand.Range(recipe.FabricationLimitMin, recipe.FabricationLimitMax + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary();
|
||||
@@ -244,7 +250,7 @@ namespace Barotrauma.Items.Components
|
||||
itemList.Enabled = true;
|
||||
if (activateButton != null)
|
||||
{
|
||||
activateButton.Text = TextManager.Get("FabricatorCreate");
|
||||
activateButton.Text = TextManager.Get(CreateButtonText);
|
||||
}
|
||||
#endif
|
||||
fabricatedItem = null;
|
||||
@@ -400,8 +406,22 @@ namespace Barotrauma.Items.Components
|
||||
quality = GetFabricatedItemQuality(fabricatedItem, user);
|
||||
}
|
||||
|
||||
int amount = (int)fabricationitemAmount.Value;
|
||||
if (fabricationLimits.ContainsKey(fabricatedItem.RecipeHash))
|
||||
{
|
||||
if (amount > fabricationLimits[fabricatedItem.RecipeHash])
|
||||
{
|
||||
amount = fabricationLimits[fabricatedItem.RecipeHash];
|
||||
fabricationLimits[fabricatedItem.RecipeHash] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
fabricationLimits[fabricatedItem.RecipeHash] -= amount;
|
||||
}
|
||||
}
|
||||
|
||||
var tempUser = user;
|
||||
for (int i = 0; i < (int)fabricationitemAmount.Value; i++)
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
float outCondition = fabricatedItem.OutCondition;
|
||||
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
|
||||
@@ -535,6 +555,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (fabricationLimits.TryGetValue(fabricableItem.RecipeHash, out int amount) && amount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return fabricableItem.RequiredItems.All(requiredItem =>
|
||||
{
|
||||
int availablePrefabsAmount = 0;
|
||||
@@ -698,7 +723,6 @@ namespace Barotrauma.Items.Components
|
||||
componentElement.Add(new XAttribute("fabricateditemidentifier", fabricatedItem.TargetItem.Identifier));
|
||||
componentElement.Add(new XAttribute("savedtimeuntilready", timeUntilReady.ToString("G", CultureInfo.InvariantCulture)));
|
||||
componentElement.Add(new XAttribute("savedrequiredtime", requiredTime.ToString("G", CultureInfo.InvariantCulture)));
|
||||
|
||||
}
|
||||
return componentElement;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -114,7 +115,7 @@ namespace Barotrauma
|
||||
|
||||
private readonly Quality qualityComponent;
|
||||
|
||||
private readonly Queue<float> impactQueue = new Queue<float>();
|
||||
private readonly ConcurrentQueue<float> impactQueue = new ConcurrentQueue<float>();
|
||||
|
||||
//a dictionary containing lists of the status effects in all the components of the item
|
||||
private readonly bool[] hasStatusEffectsOfType;
|
||||
@@ -1695,9 +1696,8 @@ namespace Barotrauma
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
while (impactQueue.Count > 0)
|
||||
while (impactQueue.TryDequeue(out float impact))
|
||||
{
|
||||
float impact = impactQueue.Dequeue();
|
||||
HandleCollision(impact);
|
||||
}
|
||||
|
||||
@@ -1933,10 +1933,7 @@ namespace Barotrauma
|
||||
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
|
||||
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
|
||||
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(impact);
|
||||
}
|
||||
impactQueue.Enqueue(impact);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,11 @@ namespace Barotrauma
|
||||
public readonly uint RecipeHash;
|
||||
public readonly int Amount;
|
||||
|
||||
/// <summary>
|
||||
/// How many of this item the fabricator can create (< 0 = unlimited)
|
||||
/// </summary>
|
||||
public readonly int FabricationLimitMin, FabricationLimitMax;
|
||||
|
||||
public FabricationRecipe(XElement element, Identifier itemPrefab)
|
||||
{
|
||||
TargetItemPrefabIdentifier = itemPrefab;
|
||||
@@ -141,6 +146,10 @@ namespace Barotrauma
|
||||
RequiresRecipe = element.GetAttributeBool("requiresrecipe", false);
|
||||
Amount = element.GetAttributeInt("amount", 1);
|
||||
|
||||
int limitDefault = element.GetAttributeInt("fabricationlimit", -1);
|
||||
FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault);
|
||||
FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault);
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -973,7 +982,11 @@ namespace Barotrauma
|
||||
|
||||
public int? GetMinPrice()
|
||||
{
|
||||
int? minPrice = StorePrices.Values.Min(p => p.Price);
|
||||
int? minPrice = null;
|
||||
if (StorePrices != null && StorePrices.Any())
|
||||
{
|
||||
minPrice = StorePrices.Values.Min(p => p.Price);
|
||||
}
|
||||
if (minPrice.HasValue)
|
||||
{
|
||||
if (DefaultPrice != null)
|
||||
|
||||
@@ -831,7 +831,13 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
List<Point> ruinPositions = new List<Point>();
|
||||
for (int i = 0; i < GenerationParams.RuinCount; i++)
|
||||
int ruinCount = GenerationParams.RuinCount;
|
||||
if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false)
|
||||
{
|
||||
ruinCount = Math.Max(ruinCount, 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ruinCount; i++)
|
||||
{
|
||||
Point ruinSize = new Point(5000);
|
||||
int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2);
|
||||
|
||||
@@ -387,14 +387,14 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(3, IsPropertySaveable.Yes, description: "Minimum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)]
|
||||
[Serialize(10, IsPropertySaveable.Yes, description: "Minimum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)]
|
||||
public int AbyssResourceClustersMin
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(20, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)]
|
||||
[Serialize(50, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)]
|
||||
public int AbyssResourceClustersMax
|
||||
{
|
||||
get;
|
||||
|
||||
@@ -257,7 +257,7 @@ namespace Barotrauma
|
||||
}
|
||||
DailySpecials.Clear();
|
||||
int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount();
|
||||
for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++)
|
||||
for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
|
||||
{
|
||||
if (availableStock.None()) { break; }
|
||||
var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
|
||||
@@ -266,7 +266,7 @@ namespace Barotrauma
|
||||
availableStock.Remove(item);
|
||||
}
|
||||
RequestedGoods.Clear();
|
||||
for (int i = 0; i < RequestedGoodsCount; i++)
|
||||
for (int i = 0; i < Location.RequestedGoodsCount; i++)
|
||||
{
|
||||
var item = ItemPrefab.Prefabs.GetRandom(p =>
|
||||
p.CanBeSold && !RequestedGoods.Contains(p) &&
|
||||
@@ -359,8 +359,8 @@ namespace Barotrauma
|
||||
/// How many map progress steps it takes before the discounts should be updated.
|
||||
/// </summary>
|
||||
private const int SpecialsUpdateInterval = 3;
|
||||
private const int DailySpecialsCount = 3;
|
||||
private const int RequestedGoodsCount = 3;
|
||||
private int DailySpecialsCount => Type.DailySpecialsCount;
|
||||
private int RequestedGoodsCount => Type.RequestedGoodsCount;
|
||||
private int StepsSinceSpecialsUpdated { get; set; }
|
||||
public HashSet<Identifier> StoreIdentifiers { get; } = new HashSet<Identifier>();
|
||||
|
||||
@@ -1226,7 +1226,7 @@ namespace Barotrauma
|
||||
|
||||
public int GetExtraSpecialSalesCount()
|
||||
{
|
||||
var characters = GameSession.GetSessionCrewCharacters();
|
||||
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
if (!characters.Any()) { return 0; }
|
||||
return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
|
||||
}
|
||||
@@ -1252,7 +1252,7 @@ namespace Barotrauma
|
||||
Discovered = true;
|
||||
if (checkTalents)
|
||||
{
|
||||
GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this)));
|
||||
GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Barotrauma.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -21,8 +19,9 @@ namespace Barotrauma
|
||||
//<name, commonness>
|
||||
private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs;
|
||||
private readonly float totalHireableWeight;
|
||||
|
||||
public Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
|
||||
|
||||
public readonly Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
|
||||
public readonly Dictionary<int, int> MinCountPerZone = new Dictionary<int, int>();
|
||||
|
||||
public readonly LocalizedString Name;
|
||||
|
||||
@@ -65,7 +64,7 @@ namespace Barotrauma
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
public string ReplaceInRadiation { get; }
|
||||
|
||||
public Sprite Sprite { get; private set; }
|
||||
@@ -86,6 +85,8 @@ namespace Barotrauma
|
||||
/// In percentages
|
||||
/// </summary>
|
||||
public int StorePriceModifierRange { get; } = 5;
|
||||
public int DailySpecialsCount { get; } = 1;
|
||||
public int RequestedGoodsCount { get; } = 1;
|
||||
|
||||
public List<StoreBalanceStatus> StoreBalanceStatuses { get; } = new List<StoreBalanceStatus>()
|
||||
{
|
||||
@@ -144,7 +145,7 @@ namespace Barotrauma
|
||||
names = new List<string>() { "Name file not found" };
|
||||
}
|
||||
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", new string[] { "" });
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
|
||||
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
|
||||
{
|
||||
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
|
||||
@@ -152,12 +153,26 @@ namespace Barotrauma
|
||||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
|
||||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone0index: zone0commonness, zone1index: zone1commonness\"");
|
||||
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
|
||||
break;
|
||||
}
|
||||
CommonnessPerZone[zoneIndex] = zoneCommonness;
|
||||
}
|
||||
|
||||
string[] minCountPerZoneStrs = element.GetAttributeStringArray("mincountperzone", Array.Empty<string>());
|
||||
foreach (string minCountPerZoneStr in minCountPerZoneStrs)
|
||||
{
|
||||
string[] splitMinCountPerZone = minCountPerZoneStr.Split(':');
|
||||
if (splitMinCountPerZone.Length != 2 ||
|
||||
!int.TryParse(splitMinCountPerZone[0].Trim(), out int zoneIndex) ||
|
||||
!int.TryParse(splitMinCountPerZone[1].Trim(), out int minCount))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read minimum count values for location type \"" + Identifier + "\" - minimum counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"");
|
||||
break;
|
||||
}
|
||||
MinCountPerZone[zoneIndex] = minCount;
|
||||
}
|
||||
|
||||
var hireableJobs = new List<(Identifier, float)>();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
@@ -205,6 +220,8 @@ namespace Barotrauma
|
||||
StoreBalanceStatuses.Add(new StoreBalanceStatus(percentage, modifier, color));
|
||||
}
|
||||
}
|
||||
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
|
||||
RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,16 +218,24 @@ namespace Barotrauma
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.Type.Identifier != "city" &&
|
||||
location.Type.Identifier != "outpost")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (location.Type.Identifier != "outpost") { continue; }
|
||||
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
|
||||
{
|
||||
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
|
||||
}
|
||||
}
|
||||
//if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
|
||||
if (CurrentLocation == null)
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Type.HasOutpost) { continue; }
|
||||
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
|
||||
{
|
||||
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
|
||||
}
|
||||
}
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
|
||||
|
||||
CurrentLocation.Discover(true);
|
||||
@@ -273,6 +281,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
voronoiSites.Clear();
|
||||
Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
|
||||
foreach (GraphEdge edge in edges)
|
||||
{
|
||||
if (edge.Point1 == edge.Point2) { continue; }
|
||||
@@ -301,7 +310,24 @@ namespace Barotrauma
|
||||
Vector2 position = points[positionIndex];
|
||||
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
|
||||
int zone = GetZoneIndex(position.X);
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient), requireOutpost: false, existingLocations: Locations);
|
||||
if (!locationsPerZone.ContainsKey(zone))
|
||||
{
|
||||
locationsPerZone[zone] = new List<Location>();
|
||||
}
|
||||
|
||||
LocationType forceLocationType = null;
|
||||
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
|
||||
{
|
||||
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
|
||||
{
|
||||
forceLocationType = locationType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
|
||||
requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
|
||||
locationsPerZone[zone].Add(newLocations[i]);
|
||||
Locations.Add(newLocations[i]);
|
||||
}
|
||||
|
||||
@@ -448,8 +474,7 @@ namespace Barotrauma
|
||||
Connections[i].Locations[1];
|
||||
if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned")
|
||||
{
|
||||
#warning TODO: determinism?
|
||||
leftMostLocation.ChangeType(LocationType.Prefabs.First(lt => lt.HasOutpost && lt.Identifier != "abandoned"));
|
||||
leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"));
|
||||
}
|
||||
leftMostLocation.IsGateBetweenBiomes = true;
|
||||
Connections[i].Locked = true;
|
||||
|
||||
@@ -752,6 +752,7 @@ namespace Barotrauma
|
||||
bool solutionFound = false;
|
||||
foreach (PlacedModule module in movableModules)
|
||||
{
|
||||
if (module.ThisGap.ConnectedDoor == null && module.PreviousGap.ConnectedDoor == null) { continue; }
|
||||
Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
|
||||
Vector2 moveStep = moveDir * 50.0f;
|
||||
Vector2 currentMove = Vector2.Zero;
|
||||
@@ -1093,6 +1094,10 @@ namespace Barotrauma
|
||||
}
|
||||
thisWayPoint.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
|
||||
}
|
||||
|
||||
gapToRemove.ConnectedDoor?.Item.Remove();
|
||||
if (hallwayLength <= 1.0f) { gapToRemove?.Remove(); }
|
||||
|
||||
@@ -28,8 +28,6 @@ namespace Barotrauma
|
||||
|
||||
partial class SubmarineInfo : IDisposable
|
||||
{
|
||||
public const string SavePath = "Submarines";
|
||||
|
||||
private static List<SubmarineInfo> savedSubmarines = new List<SubmarineInfo>();
|
||||
public static IEnumerable<SubmarineInfo> SavedSubmarines => savedSubmarines;
|
||||
|
||||
@@ -578,58 +576,14 @@ namespace Barotrauma
|
||||
if (File.Exists(savedSubmarines[i].FilePath))
|
||||
{
|
||||
bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
|
||||
bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath);
|
||||
bool isInContentPackage = contentPackageSubs.Any(f => f.Path == savedSubmarines[i].FilePath);
|
||||
if (isDownloadedSub) { continue; }
|
||||
if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; }
|
||||
if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && isInContentPackage) { continue; }
|
||||
}
|
||||
savedSubmarines[i].Dispose();
|
||||
}
|
||||
|
||||
if (!Directory.Exists(SavePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(SavePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<string> filePaths;
|
||||
string[] subDirectories;
|
||||
|
||||
try
|
||||
{
|
||||
filePaths = Directory.GetFiles(SavePath).ToList();
|
||||
subDirectories = Directory.GetDirectories(SavePath).Where(s =>
|
||||
{
|
||||
DirectoryInfo dir = new DirectoryInfo(s);
|
||||
return !dir.Attributes.HasFlag(System.IO.FileAttributes.Hidden) && !dir.Name.StartsWith(".");
|
||||
}).ToArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string subDirectory in subDirectories)
|
||||
{
|
||||
try
|
||||
{
|
||||
filePaths.AddRange(Directory.GetFiles(subDirectory).ToList());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<string> filePaths = new List<string>();
|
||||
foreach (BaseSubFile subFile in contentPackageSubs)
|
||||
{
|
||||
if (!filePaths.Any(fp => fp == subFile.Path))
|
||||
@@ -643,34 +597,7 @@ namespace Barotrauma
|
||||
foreach (string path in filePaths)
|
||||
{
|
||||
var subInfo = new SubmarineInfo(path);
|
||||
if (subInfo.IsFileCorrupted)
|
||||
{
|
||||
#if CLIENT
|
||||
if (DebugConsole.IsOpen) { DebugConsole.Toggle(); }
|
||||
var deleteSubPrompt = new GUIMessageBox(
|
||||
TextManager.Get("Error"),
|
||||
TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" +
|
||||
TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name),
|
||||
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
|
||||
|
||||
string filePath = path;
|
||||
deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e);
|
||||
}
|
||||
deleteSubPrompt.Close();
|
||||
return true;
|
||||
};
|
||||
deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close;
|
||||
#endif
|
||||
}
|
||||
else
|
||||
if (!subInfo.IsFileCorrupted)
|
||||
{
|
||||
savedSubmarines.Add(subInfo);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ namespace Barotrauma.Networking
|
||||
ManageSettings = 0x200,
|
||||
ManagePermissions = 0x400,
|
||||
KarmaImmunity = 0x800,
|
||||
BuyItems = 0x1000,
|
||||
ManageMoney = 0x1000,
|
||||
SellInventoryItems = 0x2000,
|
||||
SellSubItems = 0x4000,
|
||||
CampaignStore = 0x8000,
|
||||
All = 0xFFFF
|
||||
ManageMap = 0x8000,
|
||||
ManageHires = 0x10000,
|
||||
All = 0x1FFFF
|
||||
}
|
||||
|
||||
class PermissionPreset
|
||||
|
||||
@@ -116,7 +116,8 @@ namespace Barotrauma.Networking
|
||||
StartRound,
|
||||
PurchaseAndSwitchSub,
|
||||
PurchaseSub,
|
||||
SwitchSub
|
||||
SwitchSub,
|
||||
TransferMoney
|
||||
}
|
||||
|
||||
public enum ReadyCheckState
|
||||
@@ -179,11 +180,11 @@ namespace Barotrauma.Networking
|
||||
|
||||
protected ServerSettings serverSettings;
|
||||
|
||||
public Voting Voting { get; protected set; }
|
||||
|
||||
protected TimeSpan updateInterval;
|
||||
protected DateTime updateTimer;
|
||||
|
||||
public int EndVoteCount, EndVoteMax, SubmarineVoteYesCount, SubmarineVoteNoCount, SubmarineVoteMax;
|
||||
|
||||
protected bool gameStarted;
|
||||
|
||||
protected RespawnManager respawnManager;
|
||||
|
||||
@@ -278,8 +278,6 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
ServerLog = new ServerLog(serverName);
|
||||
|
||||
Voting = new Voting();
|
||||
|
||||
Whitelist = new WhiteList();
|
||||
BanList = new BanList();
|
||||
|
||||
@@ -378,8 +376,6 @@ namespace Barotrauma.Networking
|
||||
|
||||
public ServerLog ServerLog;
|
||||
|
||||
public Voting Voting;
|
||||
|
||||
public Dictionary<Identifier, bool> MonsterEnabled { get; private set; }
|
||||
|
||||
public const int MaxExtraCargoItemsOfType = 10;
|
||||
@@ -577,34 +573,20 @@ namespace Barotrauma.Networking
|
||||
[Serialize(true, IsPropertySaveable.Yes)]
|
||||
public bool AllowVoteKick
|
||||
{
|
||||
get
|
||||
{
|
||||
return Voting.AllowVoteKick;
|
||||
}
|
||||
set
|
||||
{
|
||||
Voting.AllowVoteKick = value;
|
||||
}
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes)]
|
||||
public bool AllowEndVoting
|
||||
{
|
||||
get
|
||||
{
|
||||
return Voting.AllowEndVoting;
|
||||
}
|
||||
set
|
||||
{
|
||||
Voting.AllowEndVoting = value;
|
||||
}
|
||||
get; set;
|
||||
}
|
||||
|
||||
private bool allowRespawn;
|
||||
[Serialize(true, IsPropertySaveable.Yes)]
|
||||
public bool AllowRespawn
|
||||
{
|
||||
get { return allowRespawn; ; }
|
||||
get { return allowRespawn; }
|
||||
set
|
||||
{
|
||||
if (allowRespawn == value) { return; }
|
||||
@@ -779,7 +761,7 @@ namespace Barotrauma.Networking
|
||||
set
|
||||
{
|
||||
subSelectionMode = value;
|
||||
Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote;
|
||||
AllowSubVoting = subSelectionMode == SelectionMode.Vote;
|
||||
ServerDetailsChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -792,7 +774,7 @@ namespace Barotrauma.Networking
|
||||
set
|
||||
{
|
||||
modeSelectionMode = value;
|
||||
Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote;
|
||||
AllowModeVoting = modeSelectionMode == SelectionMode.Vote;
|
||||
ServerDetailsChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -807,14 +789,14 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
|
||||
[Serialize(0.6f, IsPropertySaveable.Yes)]
|
||||
public float SubmarineVoteRequiredRatio
|
||||
public float VoteRequiredRatio
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(30f, IsPropertySaveable.Yes)]
|
||||
public float SubmarineVoteTimeout
|
||||
public float VoteTimeout
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
@@ -928,6 +910,59 @@ namespace Barotrauma.Networking
|
||||
set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); }
|
||||
}
|
||||
|
||||
private bool allowSubVoting;
|
||||
//Don't serialize: the value is set based on SubSelectionMode
|
||||
public bool AllowSubVoting
|
||||
{
|
||||
get { return allowSubVoting; }
|
||||
set
|
||||
{
|
||||
if (value == allowSubVoting) { return; }
|
||||
allowSubVoting = value;
|
||||
#if CLIENT
|
||||
GameMain.NetLobbyScreen.SubList.Enabled = value ||
|
||||
(GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.SelectSub));
|
||||
var subVotesLabel = GameMain.NetLobbyScreen.Frame.FindChild("subvotes", true) as GUITextBlock;
|
||||
subVotesLabel.Visible = value;
|
||||
var subVisButton = GameMain.NetLobbyScreen.SubVisibilityButton;
|
||||
subVisButton.RectTransform.AbsoluteOffset
|
||||
= new Point(value ? (int)(subVotesLabel.TextSize.X + subVisButton.Rect.Width) : 0, 0);
|
||||
|
||||
GameMain.Client?.Voting.UpdateVoteTexts(null, VoteType.Sub);
|
||||
GameMain.NetLobbyScreen.SubList.Deselect();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private bool allowModeVoting;
|
||||
//Don't serialize: the value is set based on ModeSelectionMode
|
||||
public bool AllowModeVoting
|
||||
{
|
||||
get { return allowModeVoting; }
|
||||
set
|
||||
{
|
||||
if (value == allowModeVoting) { return; }
|
||||
allowModeVoting = value;
|
||||
#if CLIENT
|
||||
GameMain.NetLobbyScreen.ModeList.Enabled =
|
||||
value ||
|
||||
(GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.SelectMode));
|
||||
GameMain.NetLobbyScreen.Frame.FindChild("modevotes", true).Visible = value;
|
||||
// Disable modes that cannot be voted on
|
||||
foreach (var guiComponent in GameMain.NetLobbyScreen.ModeList.Content.Children)
|
||||
{
|
||||
if (guiComponent is GUIFrame frame)
|
||||
{
|
||||
frame.CanBeFocused = !allowModeVoting || ((GameModePreset)frame.UserData).Votable;
|
||||
}
|
||||
}
|
||||
GameMain.Client?.Voting.UpdateVoteTexts(null, VoteType.Mode);
|
||||
GameMain.NetLobbyScreen.ModeList.Deselect();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SetPassword(string password)
|
||||
{
|
||||
if (string.IsNullOrEmpty(password))
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Voting
|
||||
{
|
||||
private bool allowSubVoting, allowModeVoting;
|
||||
|
||||
public bool AllowVoteKick = true;
|
||||
|
||||
public bool AllowEndVoting = true;
|
||||
|
||||
public bool VoteRunning = false;
|
||||
|
||||
public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 };
|
||||
|
||||
private IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, List<Client> voters)
|
||||
@@ -39,12 +31,12 @@ namespace Barotrauma
|
||||
|
||||
public T HighestVoted<T>(VoteType voteType, List<Client> voters)
|
||||
{
|
||||
if (voteType == VoteType.Sub && !AllowSubVoting) return default(T);
|
||||
if (voteType == VoteType.Mode && !AllowModeVoting) return default(T);
|
||||
if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; }
|
||||
if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; }
|
||||
|
||||
IReadOnlyDictionary<T, int> voteList = GetVoteCounts<T>(voteType, voters);
|
||||
|
||||
T selected = default(T);
|
||||
T selected = default;
|
||||
int highestVotes = 0;
|
||||
foreach (KeyValuePair<T, int> votable in voteList)
|
||||
{
|
||||
@@ -71,11 +63,13 @@ namespace Barotrauma
|
||||
{
|
||||
client.ResetVotes();
|
||||
}
|
||||
|
||||
GameMain.NetworkMember.EndVoteCount = 0;
|
||||
GameMain.NetworkMember.EndVoteMax = 0;
|
||||
|
||||
#if CLIENT
|
||||
foreach (VoteType voteType in Enum.GetValues(typeof(VoteType)))
|
||||
{
|
||||
SetVoteCountYes(voteType, 0);
|
||||
SetVoteCountNo(voteType, 0);
|
||||
SetVoteCountMax(voteType, 0);
|
||||
}
|
||||
UpdateVoteTexts(connectedClients, VoteType.Mode);
|
||||
UpdateVoteTexts(connectedClients, VoteType.Sub);
|
||||
#endif
|
||||
|
||||
@@ -348,6 +348,14 @@ namespace Barotrauma.Steam
|
||||
|
||||
string val = attribute.Value.CleanUpPathCrossPlatform(correctFilenameCase: false);
|
||||
|
||||
//Handle mods that have been mangled by pre-modding-refactor
|
||||
//copying of post-modding-refactor mods (what a clusterfuck)
|
||||
int modDirStrIndex = val.IndexOf(ContentPath.ModDirStr, StringComparison.OrdinalIgnoreCase);
|
||||
if (modDirStrIndex >= 0)
|
||||
{
|
||||
val = val[modDirStrIndex..];
|
||||
}
|
||||
|
||||
//Handle really old mods (0.9.0.4-era) that might be structured as
|
||||
//%ModDir%/Mods/[NAME]/[RESOURCE]
|
||||
string fullSrcPath = Path.Combine(fileListDir, val).CleanUpPath();
|
||||
@@ -418,7 +426,7 @@ namespace Barotrauma.Steam
|
||||
File.Copy(from, to, overwrite: true);
|
||||
}
|
||||
|
||||
private static async Task CopyDirectory(string fileListDir, string modName, string from, string to)
|
||||
public static async Task CopyDirectory(string fileListDir, string modName, string from, string to)
|
||||
{
|
||||
from = Path.GetFullPath(from); to = Path.GetFullPath(to);
|
||||
Directory.CreateDirectory(to);
|
||||
|
||||
@@ -79,10 +79,10 @@ namespace Barotrauma
|
||||
{
|
||||
roundData.EnteredCrushDepth.Add(c);
|
||||
}
|
||||
else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth * 0.5f)
|
||||
else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth - 500.0f)
|
||||
{
|
||||
//all characters that have entered crush depth and are still alive get an achievement
|
||||
if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth".ToIdentifier());
|
||||
if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +426,7 @@ namespace Barotrauma
|
||||
!c.IsDead &&
|
||||
c.TeamID != CharacterTeamType.FriendlyNPC &&
|
||||
!(c.AIController is EnemyAIController) &&
|
||||
(c.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost)));
|
||||
(c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost)));
|
||||
|
||||
if (charactersInSub.Count == 1)
|
||||
{
|
||||
@@ -454,6 +454,10 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (Character character in charactersInSub)
|
||||
{
|
||||
if (roundData.EnteredCrushDepth.Contains(character))
|
||||
{
|
||||
UnlockAchievement(character, "survivecrushdepth".ToIdentifier());
|
||||
}
|
||||
if (character.Info.Job == null) { continue; }
|
||||
UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier());
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ namespace Barotrauma
|
||||
events.Remove(identifier);
|
||||
}
|
||||
|
||||
public void TryDeregister(Identifier identifier)
|
||||
{
|
||||
if (!HasEvent(identifier)) { return; }
|
||||
Deregister(identifier);
|
||||
}
|
||||
|
||||
public bool HasEvent(Identifier identifier) => events.ContainsKey(identifier);
|
||||
|
||||
public void Invoke(T data)
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Steamworks.Data;
|
||||
using Color = Microsoft.Xna.Framework.Color;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -227,7 +228,7 @@ namespace Barotrauma
|
||||
return Path.Combine(folder, saveName);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetSaveFiles(SaveType saveType, bool includeInCompatible = true)
|
||||
public static IReadOnlyList<CampaignMode.SaveInfo> GetSaveFiles(SaveType saveType, bool includeInCompatible = true)
|
||||
{
|
||||
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
|
||||
if (!Directory.Exists(folder))
|
||||
@@ -250,18 +251,61 @@ namespace Barotrauma
|
||||
files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
|
||||
}
|
||||
|
||||
if (!includeInCompatible)
|
||||
List<CampaignMode.SaveInfo> saveInfos = new List<CampaignMode.SaveInfo>();
|
||||
foreach (string file in files)
|
||||
{
|
||||
for (int i = files.Count - 1; i >= 0; i--)
|
||||
XDocument doc = LoadGameSessionDoc(file);
|
||||
if (!includeInCompatible && !IsSaveFileCompatible(doc))
|
||||
{
|
||||
XDocument doc = LoadGameSessionDoc(files[i]);
|
||||
if (!IsSaveFileCompatible(doc))
|
||||
continue;
|
||||
}
|
||||
if (doc?.Root == null)
|
||||
{
|
||||
saveInfos.Add(new CampaignMode.SaveInfo()
|
||||
{
|
||||
files.RemoveAt(i);
|
||||
FilePath = file
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
List<string> enabledContentPackageNames = new List<string>();
|
||||
|
||||
//backwards compatibility
|
||||
string enabledContentPackagePathsStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty);
|
||||
foreach (string packagePath in enabledContentPackagePathsStr.Split('|'))
|
||||
{
|
||||
if (string.IsNullOrEmpty(packagePath)) { continue; }
|
||||
//change paths to names
|
||||
string fileName = Path.GetFileNameWithoutExtension(packagePath);
|
||||
if (fileName == "filelist")
|
||||
{
|
||||
enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath)));
|
||||
}
|
||||
else
|
||||
{
|
||||
enabledContentPackageNames.Add(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
string enabledContentPackageNamesStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty);
|
||||
//split on pipes, excluding pipes preceded by \
|
||||
foreach (string packageName in Regex.Split(enabledContentPackageNamesStr, @"(?<!(?<!\\)*\\)\|"))
|
||||
{
|
||||
if (string.IsNullOrEmpty(packageName)) { continue; }
|
||||
enabledContentPackageNames.Add(packageName.Replace(@"\|", "|"));
|
||||
}
|
||||
|
||||
saveInfos.Add(new CampaignMode.SaveInfo()
|
||||
{
|
||||
FilePath = file,
|
||||
SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
|
||||
SaveTime = doc.Root.GetAttributeInt("savetime", 0),
|
||||
EnabledContentPackageNames = enabledContentPackageNames.ToArray(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return files;
|
||||
|
||||
return saveInfos;
|
||||
}
|
||||
|
||||
public static string CreateSavePath(SaveType saveType, string fileName = "Save_Default")
|
||||
|
||||
@@ -1,3 +1,53 @@
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.17.5.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
Changes:
|
||||
- Changed server browser tooltips: list all missing mods instead of just missing ones that aren't available from the workshop.
|
||||
- Players without the permission to access the bank can request money from the bank (approved by vote).
|
||||
- Modified Selkie emergency hatch: can only use it if the shuttle is flooded.
|
||||
- Added limits to how many items a vending machine can dispense and adjusted what's available in them (unstable only).
|
||||
- Reworked campaign permissions: removed BuyItems and CampaignStore permissions (no longer needed, everyone can buy), added ManageMap and ManageHires permissions, ManageCampaign allows gives you all the other campaign-related permissions.
|
||||
- Adjusted the amount of colonies in cold caverns.
|
||||
- Added a prompt to allow modders to automatically transfer their mods and submarines from before v0.17 to the new LocalMods folder.
|
||||
- Disabled loading submarines from the Submarines folder, as it has been rendered obsolete by this change.
|
||||
- Removed submarine download confirmation prompt. All subs that are required to play in a server will be downloaded automatically, which shouldn't be a problem since they're only stored temporarily.
|
||||
- Swapped the sides of server log and character info in multiplayer tab menu.
|
||||
- Doubled the rate limit for medical clinic which should reduce "No response from server" errors.
|
||||
- Rebalance mudraptors: slightly more health, less damage at head.
|
||||
- Reduce the probability for the coilgun to dismember limbs (or break armor).
|
||||
- Crawler: adjust the vitality multipliers of hands and tail from 50% to 75%.
|
||||
|
||||
Fixes:
|
||||
- Fixed multiplayer campaign saves with semicolons or pipes in their name causing "path to a save file was empty" errors in the server lobby.
|
||||
- Fixed performance drops in multiplayer when someone attacks the outpost NPCs and causes the reputation to drop.
|
||||
- Moved the showperf view to the right to prevent it from overlapping with the crew list.
|
||||
- Fixed status monitor's electrical view and item finder not showing items in docked subs.
|
||||
- The sound of crowbaring a door open can't be heard from other subs.
|
||||
- Fixed "a gaze into the abyss" achievement working unreliably. The achievement didn't unlock until you returned to 50% of the crush depth, which isn't possible in some levels (e.g. if the level starts at 3500 m and crush depth at 5000 m). Now it's unlocked if you get to 500 m above the crush depth or to the end of the level.
|
||||
- Fixed certain achievements (last man standing, lone sailor, finishing a round with a specific job) not unlocking if you're in a docked sub instead of the main sub at the end of the round.
|
||||
- Readjusted all sitting animations so that they characters shouldn't twitch anymore while sitting.
|
||||
|
||||
Fixes (unstable only):
|
||||
- Fixed mods list being configurable outside of the main menu.
|
||||
- Misc fixes to colonies (added vents, wired docking ports, adjusted misaligned stairs, fixed a bunch of gap and draw order issues).
|
||||
- Fixed outpost generator sometimes placing a hallway between modules connected by stairs in colonies.
|
||||
- Fixed crashing when saving a sub.
|
||||
- Fixed cargo missions sometimes displaying an incorrect number of crates in the description.
|
||||
- Hide the "sufficient skills to fabricate"/"insufficient skills to fabricate" labels on the vending machines.
|
||||
- Decrease the number of daily specials and requested goods for merchants to account for the multiple vendors.
|
||||
- Fixed crash in Mission.GetSalaryEligibleCrew when taking control of a bot in the multiplayer campaign.
|
||||
- Fixed tab menu blocking all other UI elements.
|
||||
- Readjusted amount of materials in abyss islands.
|
||||
- Fixed wallet UI being accessible in all multiplayer game modes.
|
||||
- Fixed flashlights emitting sparks.
|
||||
- Flipped the confirm and reset buttons in wallet UI.
|
||||
- Added a new column to multiplayer tab menu that shows the amount of money the player has in their wallet.
|
||||
|
||||
Modding:
|
||||
- Option to make missions force a ruin in the level if there isn't one.
|
||||
- Character editor: don't check the validity of the texture path when copying humans (because the path is not valid and will be parsed later). Allows creating custom human characters by copying the vanilla human (even though they are not fully supported).
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.17.4.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user