Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -0,0 +1,87 @@
#nullable enable
using System;
namespace Barotrauma
{
/// <summary>
/// Contains all available Steam timeline event icons as constants and helper methods for number icons.
/// </summary>
public static class SteamIcons
{
// Standard icons
public const string Marker = "steam_marker";
public const string Achievement = "steam_achievement";
public const string Attack = "steam_attack";
public const string Bolt = "steam_bolt";
public const string Bookmark = "steam_bookmark";
public const string Bug = "steam_bug";
public const string Cart = "steam_cart";
public const string Caution = "steam_caution";
public const string Chat = "steam_chat";
public const string Checkmark = "steam_checkmark";
public const string Chest = "steam_chest";
public const string Circle = "steam_circle";
public const string Combat = "steam_combat";
public const string Completed = "steam_completed";
public const string Crown = "steam_crown";
public const string Death = "steam_death";
public const string Defend = "steam_defend";
public const string Diamond = "steam_diamond";
public const string Edit = "steam_edit";
public const string Effect = "steam_effect";
public const string Explosion = "steam_explosion";
public const string Fix = "steam_fix";
public const string Flag = "steam_flag";
public const string Gem = "steam_gem";
public const string Group = "steam_group";
public const string Heart = "steam_heart";
public const string Info = "steam_info";
public const string Invalid = "steam_invalid";
public const string Minus = "steam_minus";
public const string Pair = "steam_pair";
public const string Person = "steam_person";
public const string Plus = "steam_plus";
public const string Purchase = "steam_purchase";
public const string Question = "steam_question";
public const string Ribbon = "steam_ribbon";
public const string Screenshot = "steam_screenshot";
public const string Scroll = "steam_scroll";
public const string Square = "steam_square";
public const string Star = "steam_star";
public const string Starburst = "steam_starburst";
public const string Timer = "steam_timer";
public const string Transfer = "steam_transfer";
public const string Triangle = "steam_triangle";
public const string Trophy = "steam_trophy";
public const string View = "steam_view";
public const string X = "steam_x";
// Common number icons
public const string Zero = "steam_0";
public const string One = "steam_1";
public const string Two = "steam_2";
public const string Three = "steam_3";
public const string Four = "steam_4";
public const string Five = "steam_5";
public const string Six = "steam_6";
public const string Seven = "steam_7";
public const string Eight = "steam_8";
public const string Nine = "steam_9";
public const string Ten = "steam_10";
/// <summary>
/// Gets the Steam icon name for a number between 0 and 99.
/// </summary>
/// <param name="number">The number to get the icon for (0-99)</param>
/// <returns>The Steam icon name in the format "steam_X"</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the number is less than 0 or greater than 99</exception>
public static string GetNumberIcon(int number)
{
if (number is < 0 or > 99)
{
throw new ArgumentOutOfRangeException(nameof(number), "Number must be between 0 and 99");
}
return $"steam_{number}";
}
}
}
@@ -92,6 +92,8 @@ namespace Barotrauma.Steam
//Maybe I'm completely wrong! All I know is that we need to handle both!
}
SteamTimelineManager.Initialize();
}
public static bool NetworkingDebugLog { get; private set; } = false;
@@ -0,0 +1,345 @@
#nullable enable
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Linq;
using Steamworks.Data;
using Steamworks;
namespace Barotrauma
{
internal static class SteamTimelineManager
{
private static Screen? prevScreen;
private static TimelineGameMode gameMode = TimelineGameMode.LoadingScreen;
/// <summary>
/// The current submarine that the controlled character is in (and has been for at least the delay amount).
/// </summary>
private static Submarine? currentSubmarine = null;
/// <summary>
/// For tracking the instantaneous switch of submarines, to reset the delay timer
/// </summary>
private static Submarine? previousTrackedSubmarine;
private static Character? trackedCharacter = null;
/// <summary>
/// Delay in seconds before the submarine state change is considered valid, triggering events.
/// </summary>
private const float SubmarineStateChangeDelay = 2.0f;
private static float submarineStateChangeTimer = 0.0f;
public enum TimelineGameMode
{
Playing,
Staging,
Menus,
LoadingScreen
}
public static void Initialize()
{
SetTimelineGameMode(TimelineGameMode.LoadingScreen);
}
public static void Update(float deltaTime)
{
PollScreenChange();
PollCharacterChange(deltaTime);
PollSubmarineChange(deltaTime);
}
private static void PollScreenChange()
{
if (!SteamManager.IsInitialized) { return; }
if (Screen.Selected == prevScreen) { return; }
TimelineGameMode newMode = Screen.Selected switch
{
GameScreen _ => TimelineGameMode.Playing,
NetLobbyScreen _ => TimelineGameMode.Staging,
EditorScreen _ => TimelineGameMode.Playing,
MainMenuScreen _ => TimelineGameMode.Menus,
_ => TimelineGameMode.LoadingScreen // Default to Menus for other screens for now
};
if (GameMain.Instance != null && GameMain.Instance.LoadingScreenOpen)
{
newMode = TimelineGameMode.LoadingScreen;
}
if (newMode == gameMode) { return; }
SetTimelineGameMode(newMode);
gameMode = newMode;
DebugConsole.NewMessage($"Timeline game mode set to {newMode}");
prevScreen = Screen.Selected;
}
private static void PollCharacterChange(float deltaTime)
{
Character? controlledCharacter = Character.Controlled;
// reset current sub state if character changes
if (controlledCharacter != trackedCharacter)
{
InstantlySetCurrentSubmarine(controlledCharacter?.Submarine ?? null);
trackedCharacter = controlledCharacter;
}
}
private static void PollSubmarineChange(float deltaTime)
{
if (!SteamManager.IsInitialized) { return; }
if (trackedCharacter == null) { return; }
if (Screen.Selected is not GameScreen) { return; }
Submarine? trackedCharacterSubmarine = trackedCharacter.Submarine;
// timer makes sure only time-stable state changes are registered
if (submarineStateChangeTimer > 0f)
{
submarineStateChangeTimer -= deltaTime;
if (submarineStateChangeTimer <= 0f)
{
// actually register our pending state change
CharacterSubChanged(trackedCharacter, trackedCharacterSubmarine);
}
}
// detect instantaneous submarine change and start the delay timer
if (previousTrackedSubmarine != trackedCharacterSubmarine)
{
submarineStateChangeTimer = SubmarineStateChangeDelay;
}
previousTrackedSubmarine = trackedCharacterSubmarine;
}
private static void InstantlySetCurrentSubmarine(Submarine? submarine)
{
currentSubmarine = submarine;
previousTrackedSubmarine = submarine;
submarineStateChangeTimer = 0f;
}
private static void CharacterSubChanged(Character character, Submarine newSubmarine)
{
if (newSubmarine == currentSubmarine) { return; }
// currentSub to none
if (currentSubmarine != null && newSubmarine == null)
{
OnCharacterLeftSubmarine(character, currentSubmarine);
}
// currentSub to newSub
else if (currentSubmarine != null && newSubmarine != null)
{
OnCharacterMovedBetweenSubmarines(character, currentSubmarine, newSubmarine);
}
//none to newSub
else if (currentSubmarine == null && newSubmarine != null)
{
OnCharacterEnteredSubmarine(character, newSubmarine);
}
currentSubmarine = newSubmarine;
}
public static void SetTimelineGameMode(TimelineGameMode mode)
{
if (!SteamManager.IsInitialized) { return; }
Steamworks.TimelineGameMode steamMode = mode switch
{
TimelineGameMode.Playing => Steamworks.TimelineGameMode.Playing,
TimelineGameMode.Staging => Steamworks.TimelineGameMode.Staging,
TimelineGameMode.Menus => Steamworks.TimelineGameMode.Menus,
TimelineGameMode.LoadingScreen => Steamworks.TimelineGameMode.LoadingScreen,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, message: null)
};
try
{
SteamTimeline.SetTimelineGameMode(steamMode);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to set timeline game mode to {mode}", e);
}
}
public static void OnPlayerDied(Character victim, CauseOfDeath causeOfDeath)
{
if (victim == null || causeOfDeath == null) { return; }
string eventTitle = $"{victim.DisplayName} died";
string causeOfDeathText = causeOfDeath.Affliction != null ?
causeOfDeath.Affliction.CauseOfDeathDescription.Value :
causeOfDeath.Type.ToString();
string eventDescription = $"{victim.DisplayName} died: {causeOfDeathText}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Death, 1);
}
public static void OnSignificantEnemyDied(Character victim, CauseOfDeath causeOfDeath)
{
string eventTitle = $"{victim.DisplayName} has died!";
string causeOfDeathText = causeOfDeath.Affliction != null ?
causeOfDeath.Affliction.CauseOfDeathDescription.Value :
causeOfDeath.Type.ToString();
string eventDescription = $"{victim.DisplayName} died: {causeOfDeathText}";
if (causeOfDeath.Killer != null)
{
eventDescription = $"{victim.DisplayName} was killed by {causeOfDeath.Killer.DisplayName}";
}
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Attack, 2);
}
public static void OnRoundStarted()
{
string eventTitle = "Round Started";
string eventDescription = "The round has started";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Marker, 0);
}
public static void OnRoundEnded()
{
string eventTitle = "Round Ended";
string eventDescription = "The round has ended";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Completed, 0);
}
public static void OnCharacterLeftSubmarine(Character character, Submarine submarine)
{
string eventTitle = $"{character.Name} Went Diving Outside";
string eventDescription = $"{character.Name} left {submarine.Info.Name}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Transfer, 1);
}
public static void OnCharacterMovedBetweenSubmarines(Character character, Submarine oldSubmarine, Submarine newSubmarine)
{
string eventTitle = $"{character.Name} Moved Between Locations";
string eventDescription = $"{character.Name} moved from {oldSubmarine.Info.Name} to {newSubmarine.Info.Name}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Transfer, 1);
}
public static void OnCharacterEnteredSubmarine(Character character, Submarine submarine)
{
string eventTitle = $"{character.Name} Entered Hull";
string eventDescription = $"{character.Name} has entered {submarine.Info.Name}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Transfer, 1);
}
public static void OnError(string errorMessage, Exception? e = null)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = "Error Occurred";
string eventDescription = $"An error was logged: {errorMessage}";
if (e != null) { eventDescription += $"\n{e.GetType().Name}"; }
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Bug, 3); // Higher priority for errors
}
public static void OnClientDisconnect(string disconnectInfo)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = $"Client Disconnected";
string eventDescription = $"{disconnectInfo}";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Bug, 2); // Maybe slightly lower priority than code errors
}
public static void OnMonsterMissionTargetsKilled(MonsterMission mission)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = $"Monsters Dispatched";
string eventDescription = $"{mission.Name}: All targets were eliminated.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Attack, 2);
}
public static void OnScanSuccessful(ScanMission mission)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = "Scan Successful";
string eventDescription = $"{mission.Name}: A scanner has successfully scanned a target.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Marker, 1);
}
public static void OnOutpostTargetEliminated(AbandonedOutpostMission mission)
{
// these don't have localization support yet, use hardcoded strings
string eventTitle = $"Target Character Eliminated";
string eventDescription = $"{mission.Name}: A target was eliminated.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Attack, 2);
}
/// <summary>
/// How often can hull breach events be created? There's often multiple breaches very close to each other, not necessary to track all of them.
/// </summary>
const float HullBreachEventInterval = 10.0f;
private static double LastHullBreachTime;
public static void OnHullBreached(Structure structure)
{
if (LastHullBreachTime > Timing.TotalTime - HullBreachEventInterval) { return; }
// only trigger this event for player subs, since beacon stations can fill the requirements at level start
if (structure.Submarine?.Info is not { IsPlayer: true }) { return; }
string eventTitle = "Major Hull Breach";
string eventDescription = $"The hull of {structure.Submarine?.Info.Name ?? "Unknown Submarine"} suffered a major breach.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Caution, 2);
LastHullBreachTime = Timing.TotalTime;
}
public static void OnMissionTargetRetrieved(Item item, Mission mission)
{
string eventTitle = $"Target Retrieved: {item.Name}";
string eventDescription = $"{mission.Name}: A target item {item.Name} was retrieved.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Checkmark, 1);
}
public static void OnMissionTargetPickedUp(Item item, Mission mission)
{
string eventTitle = $"Target Picked Up: {item.Name}";
string eventDescription = $"{mission.Name}: A target item {item.Name} was picked up.";
AddTimelineEvent(eventTitle, eventDescription, SteamIcons.Checkmark, 1);
}
public static void AddTimelineEvent(string title, string description, string icon, uint priority = 1, Submarine? submarine = null)
{
if (!SteamManager.IsInitialized) { return; }
// exit early if title, description or icon is empty
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(description) || string.IsNullOrWhiteSpace(icon))
{
DebugConsole.ThrowError("Failed to add timeline event: title, description or icon is empty");
return;
}
if (submarine != null)
{
string submarineName = submarine.Info?.DisplayName.Value ?? "Unknown Submarine";
title = title.Replace("[sub]", submarineName);
description = description.Replace("[sub]", submarineName);
}
try
{
var eventHandle = Steamworks.SteamTimeline.AddInstantaneousTimelineEvent(
title,
description,
icon,
priority,
0.0f,
Steamworks.TimelineEventClipPriority.Standard);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to add timeline event", e);
}
}
}
}
@@ -727,7 +727,9 @@ namespace Barotrauma.Steam
}
if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains))
{
if (parentList.AllSelected.All(c => c.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last()?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") && selectedMods.Length > 0 && SteamManager.IsInitialized)
static Identifier? GetButtonIconStyle(GUIComponent c) => c.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last()?.Style?.Identifier;
if (parentList.AllSelected.All(c => GetButtonIconStyle(c) is { } style && (style == "WorkshopMenu.DownloadedIcon" || style == "WorkshopMenu.InfoButtonUpdate")) &&
selectedMods.Length > 0 && SteamManager.IsInitialized)
{
contextMenuOptions.Add(new((selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), true, () =>
{
@@ -774,8 +776,7 @@ namespace Barotrauma.Steam
return true;
}
};
msgBox.Buttons[0].OnClicked += msgBox.Close;
msgBox.Buttons[1].OnClicked += (_, _) =>
msgBox.Buttons[0].OnClicked += (_, _) =>
{
if (textBox.Text == mod.Name)
{
@@ -794,6 +795,7 @@ namespace Barotrauma.Steam
return false;
}
};
msgBox.Buttons[1].OnClicked += msgBox.Close;
}
void CopyToLocal()
{
@@ -10,6 +10,7 @@ using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Steamworks;
using Steamworks.Ugc;
using Directory = Barotrauma.IO.Directory;
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
using Path = Barotrauma.IO.Path;
@@ -335,9 +336,10 @@ namespace Barotrauma.Steam
.WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value))
.WithChangeLog(changeNoteTextBox.Text)
.WithMetaData($"gameversion={localPackage.GameVersion};modversion={versionTextBox.Text}")
.WithVisibility(visibility)
.WithPreviewFile(thumbnailPath);
ugcEditor.Visibility = visibility;
CoroutineManager.StartCoroutine(
MessageBoxCoroutine((currentStepText, messageBox)
=> PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage)));