Unstable 0.17.0.0
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
static partial class SteamManager
|
||||
{
|
||||
public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID)
|
||||
{
|
||||
if (!IsInitialized || !Steamworks.SteamClient.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam;
|
||||
|
||||
DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID);
|
||||
Steamworks.BeginAuthResult startResult = Steamworks.SteamUser.BeginAuthSession(authTicketData, clientSteamID);
|
||||
if (startResult != Steamworks.BeginAuthResult.OK)
|
||||
{
|
||||
DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")");
|
||||
}
|
||||
|
||||
return startResult;
|
||||
}
|
||||
|
||||
public static void StopAuthSession(ulong clientSteamID)
|
||||
{
|
||||
if (!IsInitialized || !Steamworks.SteamClient.IsValid) return;
|
||||
|
||||
DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID);
|
||||
Steamworks.SteamUser.EndAuthSession(clientSteamID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
public partial class WorkshopMenu
|
||||
{
|
||||
private readonly struct BBWord
|
||||
{
|
||||
[Flags]
|
||||
public enum TagType
|
||||
{
|
||||
None = 0x0,
|
||||
Bold = 0x1,
|
||||
Italic = 0x2,
|
||||
Header = 0x4,
|
||||
List = 0x8,
|
||||
NewLine = 0x10
|
||||
}
|
||||
|
||||
public readonly string Text;
|
||||
public readonly Vector2 Size;
|
||||
public readonly TagType TagTypes;
|
||||
|
||||
public readonly GUIFont Font;
|
||||
|
||||
public BBWord(string text, TagType tagTypes)
|
||||
{
|
||||
Text = text;
|
||||
TagTypes = tagTypes;
|
||||
Font = tagTypes.HasFlag(TagType.Header)
|
||||
? GUIStyle.LargeFont
|
||||
: tagTypes.HasFlag(TagType.Bold)
|
||||
? GUIStyle.SubHeadingFont
|
||||
: GUIStyle.Font;
|
||||
Size = Font.MeasureString(Text);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
private static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container)
|
||||
{
|
||||
Point cachedContainerSize = Point.Zero;
|
||||
List<BBWord> bbWords = new List<BBWord>();
|
||||
Stack<BBWord.TagType> tagStack = new Stack<BBWord.TagType>();
|
||||
|
||||
void recalculate()
|
||||
{
|
||||
if (cachedContainerSize == container.Content.RectTransform.NonScaledSize) { return; }
|
||||
|
||||
bbWords.Clear();
|
||||
cachedContainerSize = container.Content.RectTransform.NonScaledSize;
|
||||
|
||||
var matches = new Stack<Match>(bbTagRegex.Matches(bbCode).Reverse());
|
||||
Match? nextTag = null;
|
||||
matches.TryPop(out nextTag);
|
||||
int wordStart = 0;
|
||||
BBWord.TagType currTagType;
|
||||
for (int i = 0; i < bbCode.Length; i++)
|
||||
{
|
||||
char currChar = bbCode[i];
|
||||
currTagType = tagStack.TryPeek(out var t) ? t : BBWord.TagType.None;
|
||||
|
||||
bool charIsCJK = TextManager.IsCJK($"{currChar}");
|
||||
bool wordEnd = char.IsWhiteSpace(currChar) || charIsCJK;
|
||||
int reachedTagLength = 0;
|
||||
if (nextTag is { Index: int tagIndex, Length: int tagLength }
|
||||
&& i == tagIndex)
|
||||
{
|
||||
reachedTagLength = tagLength;
|
||||
string tagStr = nextTag.Value.Replace("[", "").Replace("]", "").Trim();
|
||||
bool isClosing = tagStr.StartsWith("/");
|
||||
tagStr = tagStr.Replace("/", "").Trim().ToLowerInvariant();
|
||||
BBWord.TagType tagType = tagStr switch
|
||||
{
|
||||
"b" => BBWord.TagType.Bold,
|
||||
"i" => BBWord.TagType.Italic,
|
||||
"h1" => BBWord.TagType.Header,
|
||||
_ => BBWord.TagType.None
|
||||
};
|
||||
|
||||
if (tagType != BBWord.TagType.None)
|
||||
{
|
||||
if (isClosing)
|
||||
{
|
||||
if (currTagType == tagType)
|
||||
{
|
||||
tagStack.Pop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tagStack.Push(tagType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wordEnd || reachedTagLength > 0)
|
||||
{
|
||||
string word = bbCode[wordStart..i];
|
||||
if (charIsCJK) { word = bbCode[wordStart..(i + 1)]; }
|
||||
else if (char.IsWhiteSpace(currChar) && currChar != '\n') { word += " "; }
|
||||
|
||||
if (!word.IsNullOrEmpty())
|
||||
{
|
||||
bbWords.Add(new BBWord(word, currTagType));
|
||||
}
|
||||
else if (currChar == '\n')
|
||||
{
|
||||
bbWords.Add(new BBWord("", BBWord.TagType.NewLine));
|
||||
}
|
||||
|
||||
if (reachedTagLength > 0)
|
||||
{
|
||||
i += reachedTagLength - 1;
|
||||
nextTag = matches.TryPop(out var tag) ? tag : null;
|
||||
}
|
||||
|
||||
wordStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
currTagType = tagStack.TryPeek(out var ft) ? ft : BBWord.TagType.None;
|
||||
string finalWord = bbCode[wordStart..];
|
||||
if (!finalWord.IsNullOrEmpty())
|
||||
{
|
||||
bbWords.Add(new BBWord(finalWord, currTagType));
|
||||
}
|
||||
}
|
||||
|
||||
void draw(SpriteBatch spriteBatch, GUICustomComponent component)
|
||||
{
|
||||
recalculate();
|
||||
Vector2 currPos = Vector2.Zero;
|
||||
Vector2 rectPos = component.Rect.Location.ToVector2();
|
||||
for (int i = 0; i < bbWords.Count; i++)
|
||||
{
|
||||
var bbWord = bbWords[i];
|
||||
if (currPos.X > 0.0f
|
||||
&& currPos.X + bbWord.Size.X >= component.Rect.Width)
|
||||
{
|
||||
//wrap because we went over width limit
|
||||
currPos = (0.0f, currPos.Y + bbWord.Size.Y);
|
||||
}
|
||||
|
||||
bbWord.Font.DrawString(
|
||||
spriteBatch,
|
||||
bbWord.Text,
|
||||
(currPos + rectPos).ToPoint().ToVector2(),
|
||||
GUIStyle.TextColorNormal,
|
||||
forceUpperCase: ForceUpperCase.No,
|
||||
italics: bbWord.TagTypes.HasFlag(BBWord.TagType.Italic));
|
||||
bool breakLine
|
||||
= bbWord.TagTypes.HasFlag(BBWord.TagType.NewLine)
|
||||
|| (i < bbWords.Count - 1 &&
|
||||
bbWords[i + 1].TagTypes.HasFlag(BBWord.TagType.Header) !=
|
||||
bbWord.TagTypes.HasFlag(BBWord.TagType.Header));
|
||||
if (breakLine)
|
||||
{
|
||||
//break line because of a header change or newline was found
|
||||
currPos = (0.0f, currPos.Y + bbWord.Size.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
currPos.X += bbWord.Size.X;
|
||||
}
|
||||
}
|
||||
|
||||
component.RectTransform.NonScaledSize
|
||||
= (component.RectTransform.NonScaledSize.X,
|
||||
(int)(currPos.Y + bbWords.LastOrDefault().Size.Y));
|
||||
component.RectTransform.RelativeSize
|
||||
= component.RectTransform.NonScaledSize.ToVector2() / component.Parent.Rect.Size.ToVector2();
|
||||
}
|
||||
|
||||
return new GUICustomComponent(new RectTransform(Vector2.One, container.Content.RectTransform),
|
||||
onDraw: draw);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
public partial class WorkshopMenu
|
||||
{
|
||||
private string ExtractTitle(ItemOrPackage itemOrPackage)
|
||||
=> itemOrPackage.TryGet(out ContentPackage package)
|
||||
? package.Name
|
||||
: ((Steamworks.Ugc.Item)itemOrPackage).Title;
|
||||
|
||||
private void CreateWorkshopItemDetailContainer(
|
||||
GUIFrame parent,
|
||||
out GUIListBox outerContainer,
|
||||
Action<ItemOrPackage, GUIFrame> onSelected,
|
||||
Action onDeselected,
|
||||
out Action<ItemOrPackage> select,
|
||||
out Action deselect)
|
||||
{
|
||||
ItemOrPackage? selectedItemOrPackage = null;
|
||||
|
||||
GUIListBox outContainer = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform),
|
||||
isHorizontal: true,
|
||||
style: null)
|
||||
{
|
||||
ScrollBarEnabled = false,
|
||||
ScrollBarVisible = false,
|
||||
HoverCursor = CursorState.Default
|
||||
};
|
||||
outerContainer = outContainer;
|
||||
|
||||
var selectedLayout =
|
||||
new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform));
|
||||
var selectedHeaderLayout =
|
||||
new GUILayoutGroup(new RectTransform((1.0f, 0.05f), selectedLayout.RectTransform),
|
||||
isHorizontal: true,
|
||||
childAnchor: Anchor.CenterLeft);
|
||||
|
||||
void deselectMethod()
|
||||
{
|
||||
if (selectedItemOrPackage is null) { return; }
|
||||
selectedItemOrPackage = null;
|
||||
onDeselected();
|
||||
}
|
||||
|
||||
deselect = deselectMethod;
|
||||
|
||||
var backButton =
|
||||
new GUIButton(new RectTransform((0.04f, 1.0f), selectedHeaderLayout.RectTransform),
|
||||
style: "GUIButtonToggleLeft")
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
deselectMethod();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var padding = new GUIFrame(new RectTransform((1.0f, 0.005f), selectedLayout.RectTransform), style: null);
|
||||
var selectedFrame = new GUIFrame(new RectTransform((1.0f, 0.945f), selectedLayout.RectTransform),
|
||||
style: null);
|
||||
|
||||
var selectionScroller = new GUICustomComponent(
|
||||
new RectTransform(Vector2.Zero, outerContainer.Parent.RectTransform),
|
||||
onUpdate: (deltaTime, component) =>
|
||||
{
|
||||
float targetScroll = selectedItemOrPackage is null
|
||||
? 0.0f
|
||||
: 1.0f;
|
||||
outContainer.ScrollBar.BarScroll
|
||||
= MathUtils.NearlyEqual(targetScroll, outContainer.ScrollBar.BarScroll)
|
||||
? targetScroll
|
||||
: MathHelper.Lerp(outContainer.ScrollBar.BarScroll, targetScroll, 0.3f);
|
||||
});
|
||||
|
||||
select = itemOrPackage =>
|
||||
{
|
||||
//showInSteamButton.Visible = itemOrPackage.TryGet(out Steamworks.Ugc.Item _);
|
||||
//selectedItem = itemOrPackage;
|
||||
//selectedTitle.Text = ExtractTitle(itemOrPackage);
|
||||
selectedFrame.ClearChildren();
|
||||
|
||||
//Jank to fix mouserect not clamping properly
|
||||
//when shifting all elements to the left
|
||||
var dropdowns = outContainer.Content.GetAllChildren<GUIDropDown>().ToArray();
|
||||
var allChildren = outContainer.Content.GetAllChildren()
|
||||
.Concat(selectedFrame.GetAllChildren());
|
||||
allChildren.ForEach(c =>
|
||||
{
|
||||
//c.CascadingMouseRectClamp = !dropdowns.Any(dd => dd.IsParentOf(c) || dd.ListBox.IsParentOf(c));
|
||||
//c.CanBeFocused = c.CanBeFocused || !c.CascadingMouseRectClamp;
|
||||
c.ClampMouseRectToParent = !(c.Parent?.Parent is GUIDropDown);
|
||||
}
|
||||
);
|
||||
|
||||
selectedItemOrPackage = itemOrPackage;
|
||||
onSelected(itemOrPackage, selectedFrame);
|
||||
};
|
||||
}
|
||||
|
||||
private void CreateWorkshopItemList(
|
||||
GUIFrame parent,
|
||||
out GUIListBox outerContainer,
|
||||
out GUIListBox workshopItemList,
|
||||
Action<Steamworks.Ugc.Item, GUIFrame> onSelected)
|
||||
=> CreateWorkshopItemOrPackageList(
|
||||
parent,
|
||||
out outerContainer,
|
||||
out workshopItemList,
|
||||
onSelected: (ItemOrPackage itemOrPackage, GUIFrame frame)
|
||||
=> onSelected((Steamworks.Ugc.Item)itemOrPackage, frame));
|
||||
|
||||
private GUIButton CreateShowInSteamButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT)
|
||||
=> new GUIButton(
|
||||
rectT,
|
||||
TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall")
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
SteamManager.OverlayCustomURL(workshopItem.Url);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private GUIButton? CreateShowInSteamButton(ItemOrPackage itemOrPackage)
|
||||
=> itemOrPackage.TryGet(out Steamworks.Ugc.Item workshopItem)
|
||||
? CreateShowInSteamButton(workshopItem)
|
||||
: null;
|
||||
|
||||
private void CreateWorkshopItemOrPackageList(
|
||||
GUIFrame parent,
|
||||
out GUIListBox outerContainer,
|
||||
out GUIListBox workshopItemList,
|
||||
Action<ItemOrPackage, GUIFrame> onSelected)
|
||||
{
|
||||
GUIListBox? itemList = null;
|
||||
|
||||
CreateWorkshopItemDetailContainer(
|
||||
parent,
|
||||
out outerContainer,
|
||||
onSelected: onSelected,
|
||||
onDeselected: () => itemList?.Deselect(),
|
||||
out var select, out var deselect);
|
||||
|
||||
itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform));
|
||||
itemList.RectTransform.SetAsFirstChild();
|
||||
workshopItemList = itemList;
|
||||
|
||||
var deselectCarrier
|
||||
= CreateActionCarrier(outerContainer.Content, nameof(deselect).ToIdentifier(), deselect);
|
||||
|
||||
itemList.OnSelected = (component, userData) =>
|
||||
{
|
||||
//Don't select if hitting the subscribe button
|
||||
if (GUI.MouseOn.Parent != itemList.Content) { return false; }
|
||||
|
||||
if (!(userData is ItemOrPackage itemOrPackage)) { return false; }
|
||||
|
||||
select(itemOrPackage);
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
private void AddUnpublishedMods(ISet<Steamworks.Ugc.Item> workshopItems)
|
||||
{
|
||||
//Users that don't have a proper license cannot publish Workshop items
|
||||
//(see https://partner.steamgames.com/doc/features/workshop#15)
|
||||
void clearWithMessage(LocalizedString message)
|
||||
{
|
||||
selfModsList.ClearChildren();
|
||||
var messageFrame = new GUIFrame(new RectTransform(Vector2.One, selfModsList.Content.RectTransform),
|
||||
style: null)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
new GUITextBlock(new RectTransform((0.5f, 1.0f), messageFrame.RectTransform, Anchor.Center),
|
||||
text: message,
|
||||
textAlignment: Alignment.Center,
|
||||
wrap: true,
|
||||
font: GUIStyle.Font);
|
||||
}
|
||||
|
||||
if (SteamManager.IsFreeWeekend())
|
||||
{
|
||||
clearWithMessage(TextManager.Get("FreeWeekendCantPublish"));
|
||||
return;
|
||||
}
|
||||
if (SteamManager.IsFamilyShared())
|
||||
{
|
||||
clearWithMessage(TextManager.Get("FamilySharedCantPublish"));
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime getEditTime(ContentPackage p)
|
||||
=> File.GetLastWriteTime(Path.GetDirectoryName(p.Path)!);
|
||||
|
||||
//Find local packages associated with the Workshop items if available
|
||||
(Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems
|
||||
.Select(item => (item,
|
||||
(ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p
|
||||
=> p.SteamWorkshopId != 0 && p.SteamWorkshopId == item.Id)))
|
||||
//Sort the pairs by last local edit time if available
|
||||
.OrderBy(t => t.Item2 == null)
|
||||
.ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime)
|
||||
.ToArray();
|
||||
|
||||
int indexOfUserDataInPublishedItemsArray(object userData)
|
||||
=> publishedItems.IndexOf(t
|
||||
=> t.WorkshopItem.Id == ((Steamworks.Ugc.Item)(userData as ItemOrPackage)).Id);
|
||||
|
||||
//Take the existing GUI items that are in the list and sort to match the order of publishedItems
|
||||
var publishedGuiComponents = selfModsList.Content.Children.OrderBy(c => indexOfUserDataInPublishedItemsArray(c.UserData)).ToArray();
|
||||
|
||||
//Get mods that haven't been published and add them to the list
|
||||
var unpublishedMods = ContentPackageManager.LocalPackages
|
||||
.Where(p => p.SteamWorkshopId == 0 || !publishedItems.Any(item => item.WorkshopItem.Id == p.SteamWorkshopId))
|
||||
.OrderByDescending(getEditTime).ToArray();
|
||||
|
||||
if (unpublishedMods.Any())
|
||||
{
|
||||
var unpublishedHeader
|
||||
= new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform),
|
||||
TextManager.Get("UnpublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
|
||||
}
|
||||
|
||||
foreach (var unpublishedMod in unpublishedMods)
|
||||
{
|
||||
var unpublishedFrame = new GUIFrame(
|
||||
new RectTransform((1.0f, 1.0f / 5.5f), selfModsList.Content.RectTransform),
|
||||
style: "ListBoxElement")
|
||||
{
|
||||
UserData = (ItemOrPackage)unpublishedMod
|
||||
};
|
||||
var unpublishedLayout
|
||||
= new GUILayoutGroup(new RectTransform(Vector2.One, unpublishedFrame.RectTransform),
|
||||
isHorizontal: true)
|
||||
{
|
||||
Stretch = true,
|
||||
RelativeSpacing = 0.02f
|
||||
};
|
||||
var unpublishedPadding
|
||||
= new GUIFrame(
|
||||
new RectTransform(Vector2.One, unpublishedLayout.RectTransform,
|
||||
scaleBasis: ScaleBasis.BothHeight), style: null)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
var unpublishedTextBlock
|
||||
= new GUITextBlock(new RectTransform(Vector2.One, unpublishedLayout.RectTransform),
|
||||
$"{unpublishedMod.Name}\n\n" +
|
||||
TextManager.GetWithVariable("LastLocalEditTime",
|
||||
"[datetime]",
|
||||
getEditTime(unpublishedMod).ToString()),
|
||||
font: GUIStyle.Font)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
}
|
||||
|
||||
if (publishedGuiComponents.Any())
|
||||
{
|
||||
var publishedHeader
|
||||
= new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform),
|
||||
TextManager.Get("PublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
|
||||
}
|
||||
|
||||
foreach (var c in publishedGuiComponents)
|
||||
{
|
||||
c.SetAsLastChild();
|
||||
var textBlock = (c.FindChild(b => b is GUITextBlock, recursive: true) as GUITextBlock)!;
|
||||
textBlock.Text += $"\n";
|
||||
|
||||
int index = indexOfUserDataInPublishedItemsArray(c.UserData);
|
||||
(Steamworks.Ugc.Item workshopItem, ContentPackage? localMod) = publishedItems[index];
|
||||
if (localMod != null)
|
||||
{
|
||||
textBlock.Text += $"\n" + TextManager.GetWithVariable("LastLocalEditTime", "[datetime]", getEditTime(localMod).ToString());
|
||||
}
|
||||
textBlock.Text += $"\n" + TextManager.GetWithVariable("LatestPublishTime", "[datetime]", workshopItem.LatestUpdateTime.ToLocalTime().ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static (GUIButton Button, GUIFrame Sprite) CreatePaddedButton(RectTransform rectT, string style, float spriteScale)
|
||||
{
|
||||
var button = new GUIButton(
|
||||
rectT,
|
||||
style: null);
|
||||
|
||||
var sprite = new GUIFrame(
|
||||
new RectTransform(Vector2.One * spriteScale, button.RectTransform, Anchor.Center),
|
||||
style: style)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
|
||||
return (button, sprite);
|
||||
}
|
||||
|
||||
private static void CreateSubscribeButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT, float spriteScale)
|
||||
{
|
||||
const string plusButton = "GUIPlusButton";
|
||||
const string minusButton = "GUIMinusButton";
|
||||
|
||||
LocalizedString subscribeTooltip = TextManager.Get("DownloadButton");
|
||||
LocalizedString unsubscribeTooptip = TextManager.Get("WorkshopItemUnsubscribe");
|
||||
|
||||
var (subscribeButton, subscribeButtonSprite) = CreatePaddedButton(rectT, plusButton, spriteScale);
|
||||
subscribeButton.ToolTip = subscribeTooltip;
|
||||
|
||||
subscribeButton.OnClicked = (button, o) =>
|
||||
{
|
||||
if (!workshopItem.IsSubscribed)
|
||||
{
|
||||
workshopItem.Subscribe();
|
||||
TaskPool.Add($"DownloadSubscribedItem{workshopItem.Id}",
|
||||
SteamManager.Workshop.ForceRedownload(workshopItem),
|
||||
t => { });
|
||||
}
|
||||
else
|
||||
{
|
||||
workshopItem.Unsubscribe();
|
||||
SteamManager.Workshop.Uninstall(workshopItem);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
var buttonStyleUpdater = new GUICustomComponent(
|
||||
new RectTransform(Vector2.Zero, subscribeButton.RectTransform),
|
||||
onUpdate: (deltaTime, component) =>
|
||||
{
|
||||
if (subscribeButtonSprite.Style is { Identifier: { } styleId })
|
||||
{
|
||||
if (workshopItem.IsSubscribed && styleId != minusButton)
|
||||
{
|
||||
subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(minusButton));
|
||||
subscribeButton.ToolTip = unsubscribeTooptip;
|
||||
}
|
||||
if (!workshopItem.IsSubscribed && styleId != plusButton)
|
||||
{
|
||||
subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(plusButton));
|
||||
subscribeButton.ToolTip = subscribeTooltip;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
float displayedDownloadAmount = workshopItem.DownloadAmount;
|
||||
var downloadProgressBar = new GUICustomComponent(
|
||||
new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center),
|
||||
onDraw: (spriteBatch, component) =>
|
||||
{
|
||||
bool visible = workshopItem.IsSubscribed
|
||||
&& (workshopItem.IsDownloading
|
||||
|| workshopItem.IsDownloadPending
|
||||
|| !MathUtils.NearlyEqual(workshopItem.DownloadAmount, displayedDownloadAmount));
|
||||
if (!visible) { return; }
|
||||
|
||||
void drawSection(float amount, Color color, float thickness)
|
||||
=> GUI.DrawDonutSection(
|
||||
spriteBatch,
|
||||
component.Rect.Center.ToVector2() + (0, 1),
|
||||
new Range<float>(component.Rect.Width * 0.55f - thickness * 0.5f, component.Rect.Width * 0.55f + thickness * 0.5f),
|
||||
amount * MathF.PI * 2.0f,
|
||||
color);
|
||||
|
||||
void drawSectionFuzzy(float amount, Color color, float thickness)
|
||||
{
|
||||
drawSection(amount, color, thickness);
|
||||
drawSection(amount, color * 0.6f, thickness + 0.5f);
|
||||
drawSection(amount, color * 0.3f, thickness + 1.0f);
|
||||
}
|
||||
|
||||
drawSectionFuzzy(1.0f, Color.Lerp(Color.Black, GUIStyle.Blue, 0.2f), component.Rect.Width * 0.25f);
|
||||
drawSectionFuzzy(1.0f, Color.Black, component.Rect.Width * 0.15f);
|
||||
drawSectionFuzzy(displayedDownloadAmount, GUIStyle.Green, component.Rect.Width * 0.08f);
|
||||
},
|
||||
onUpdate: (deltaTime, component) =>
|
||||
{
|
||||
displayedDownloadAmount = Math.Min(
|
||||
workshopItem.DownloadAmount,
|
||||
MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f));
|
||||
})
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
}
|
||||
|
||||
private void PopulateItemList(GUIListBox itemListBox, Task<ISet<Steamworks.Ugc.Item>> items, bool includeSubscribeButton, Action<ISet<Steamworks.Ugc.Item>>? onFill = null)
|
||||
{
|
||||
itemListBox.ClearChildren();
|
||||
itemListBox.Deselect();
|
||||
itemListBox.ScrollBar.BarScroll = 0.0f;
|
||||
TaskPool.Add("PopulateTabWithItemList", items,
|
||||
(t) =>
|
||||
{
|
||||
taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc;
|
||||
itemListBox.ClearChildren();
|
||||
var workshopItems = ((Task<ISet<Steamworks.Ugc.Item>>)t).Result;
|
||||
foreach (var workshopItem in workshopItems)
|
||||
{
|
||||
var itemFrame = new GUIFrame(
|
||||
new RectTransform((1.0f, 1.0f / 5.5f), itemListBox.Content.RectTransform),
|
||||
style: "ListBoxElement")
|
||||
{
|
||||
UserData = (ItemOrPackage)workshopItem
|
||||
};
|
||||
var itemLayout = new GUILayoutGroup(
|
||||
new RectTransform(Vector2.One, itemFrame.RectTransform),
|
||||
isHorizontal: true, childAnchor: Anchor.CenterLeft)
|
||||
{
|
||||
Stretch = true
|
||||
};
|
||||
|
||||
var thumbnailContainer
|
||||
= CreateThumbnailContainer(itemLayout, Vector2.One, ScaleBasis.BothHeight);
|
||||
CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer);
|
||||
thumbnailContainer.CanBeFocused = false;
|
||||
thumbnailContainer.GetAllChildren().ForEach(c => c.CanBeFocused = false);
|
||||
|
||||
var title = new GUITextBlock(
|
||||
new RectTransform(Vector2.One, itemLayout.RectTransform),
|
||||
workshopItem.Title, font: GUIStyle.Font)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
|
||||
if (includeSubscribeButton)
|
||||
{
|
||||
CreateSubscribeButton(workshopItem, new RectTransform(Vector2.One, itemLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), spriteScale: 0.4f);
|
||||
}
|
||||
}
|
||||
onFill?.Invoke(workshopItems);
|
||||
});
|
||||
}
|
||||
|
||||
private GUIFrame CreateThumbnailContainer(
|
||||
GUIComponent parent,
|
||||
Vector2 relativeSize,
|
||||
ScaleBasis scaleBasis)
|
||||
=> new GUIFrame(new RectTransform(relativeSize, parent.RectTransform, scaleBasis: scaleBasis),
|
||||
style: "GUIFrameListBox");
|
||||
|
||||
private SteamManager.Workshop.ItemThumbnail CreateItemThumbnail(
|
||||
in Steamworks.Ugc.Item workshopItem,
|
||||
CancellationToken cancellationToken,
|
||||
GUIFrame thumbnailContainer)
|
||||
{
|
||||
var thumbnail = new SteamManager.Workshop.ItemThumbnail(workshopItem, cancellationToken);
|
||||
itemThumbnails.Add(thumbnail);
|
||||
CreateAsyncThumbnailComponent(thumbnailContainer, () => thumbnail.Texture, () => thumbnail.Loading);
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
private GUICustomComponent CreateAsyncThumbnailComponent(GUIFrame thumbnailContainer, Func<Texture2D?> textureGetter, Func<bool> throbberEnabled)
|
||||
{
|
||||
int randomThrobberOffset = Rand.Range(0, 10, Rand.RandSync.Unsynced);
|
||||
return new GUICustomComponent(
|
||||
new RectTransform(Vector2.One, thumbnailContainer.RectTransform, Anchor.Center),
|
||||
onDraw: (spriteBatch, component) =>
|
||||
{
|
||||
Rectangle rect = component.Rect;
|
||||
Texture2D? texture = textureGetter();
|
||||
if (texture != null)
|
||||
{
|
||||
rect.Location += (4, 4);
|
||||
rect.Size -= (8, 8);
|
||||
Point destinationSizeMaxWidth = (rect.Width, rect.Width * texture.Height / texture.Width);
|
||||
Point destinationSizeMaxHeight = (rect.Height * texture.Width / texture.Height, rect.Height);
|
||||
Point destinationSize = destinationSizeMaxHeight.X > rect.Width
|
||||
? destinationSizeMaxWidth
|
||||
: destinationSizeMaxHeight;
|
||||
Rectangle destinationRectangle = new Rectangle(
|
||||
rect.Center.X - destinationSize.X / 2,
|
||||
rect.Center.Y - destinationSize.Y / 2,
|
||||
destinationSize.X,
|
||||
destinationSize.Y);
|
||||
spriteBatch.Draw(texture, destinationRectangle, Color.White);
|
||||
}
|
||||
else if (throbberEnabled())
|
||||
{
|
||||
var sheet = GUIStyle.GenericThrobber;
|
||||
Vector2 pos = rect.Center.ToVector2() - Vector2.One * rect.Height * 0.4f;
|
||||
sheet.Draw(spriteBatch, ((int)Math.Floor(Timing.TotalTime * 24.0f) + randomThrobberOffset) % sheet.FrameCount, pos, Color.White,
|
||||
origin: Vector2.Zero, rotate: 0.0f,
|
||||
scale: Vector2.One * component.Rect.Height / sheet.FrameSize.ToVector2() * 0.8f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private GUIListBox CreateTagsList(IEnumerable<Identifier> tags, RectTransform rectT, bool canBeFocused)
|
||||
{
|
||||
var tagsList
|
||||
= new GUIListBox(rectT, style: null, isHorizontal: false)
|
||||
{
|
||||
UseGridLayout = true,
|
||||
ScrollBarEnabled = false,
|
||||
ScrollBarVisible = false,
|
||||
HideChildrenOutsideFrame = false,
|
||||
Spacing = GUI.IntScale(4)
|
||||
};
|
||||
tagsList.Content.ClampMouseRectToParent = false;
|
||||
foreach (Identifier tag in tags)
|
||||
{
|
||||
var tagBtn = new GUIButton(
|
||||
new RectTransform(new Vector2(0.25f, 1.0f / 8.0f), tagsList.Content.RectTransform,
|
||||
anchor: Anchor.TopLeft),
|
||||
TextManager.Get($"workshop.contenttag.{tag.Value.RemoveWhitespace()}")
|
||||
.Fallback(tag.Value.CapitaliseFirstInvariant()), style: "GUIButtonRound")
|
||||
{
|
||||
CanBeFocused = canBeFocused,
|
||||
Selected = !canBeFocused,
|
||||
UserData = tag
|
||||
};
|
||||
tagBtn.RectTransform.NonScaledSize
|
||||
= tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(5));
|
||||
tagBtn.RectTransform.IsFixedSize = true;
|
||||
tagBtn.ClampMouseRectToParent = false;
|
||||
}
|
||||
|
||||
return tagsList;
|
||||
}
|
||||
|
||||
private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame)
|
||||
{
|
||||
taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc;
|
||||
|
||||
var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform));
|
||||
|
||||
var headerLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.1f), verticalLayout.RectTransform),
|
||||
isHorizontal: true) { Stretch = true };
|
||||
|
||||
var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform));
|
||||
|
||||
var selectedTitle =
|
||||
new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title,
|
||||
font: GUIStyle.LargeFont);
|
||||
|
||||
var author = workshopItem.Owner;
|
||||
var authorButton = new GUIButton(new RectTransform((1.0f, 0.5f),
|
||||
titleAndAuthorLayout.RectTransform),
|
||||
style: null,
|
||||
textAlignment: Alignment.CenterLeft)
|
||||
{
|
||||
ForceUpperCase = ForceUpperCase.No,
|
||||
Font = GUIStyle.SubHeadingFont,
|
||||
TextColor = GUIStyle.TextColorNormal,
|
||||
HoverTextColor = Color.White,
|
||||
SelectedTextColor = GUIStyle.TextColorNormal,
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
SteamManager.OverlayCustomURL(
|
||||
$"https://steamcommunity.com/profiles/{author.Id}/myworkshopfiles/?appid={SteamManager.AppID}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var authorPadding = authorButton.GetChild<GUITextBlock>().Padding;
|
||||
|
||||
RectTransform rightSideButtonRectT()
|
||||
=> new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight);
|
||||
|
||||
var (reinstallButton, reinstallSprite) = CreatePaddedButton(
|
||||
rightSideButtonRectT(),
|
||||
"GUIReloadButton",
|
||||
spriteScale: 0.8f);
|
||||
reinstallButton.ToolTip = TextManager.Get("WorkshopItemReinstall");
|
||||
reinstallButton.OnClicked += (button, o) =>
|
||||
{
|
||||
SteamManager.Workshop.Uninstall(workshopItem);
|
||||
TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.ForceRedownload(workshopItem), t => { });
|
||||
return false;
|
||||
};
|
||||
var reinstallButtonUpdater = new GUICustomComponent(
|
||||
new RectTransform(Vector2.Zero, reinstallButton.RectTransform),
|
||||
onUpdate: (f, component) =>
|
||||
{
|
||||
reinstallButton.Visible = workshopItem.IsSubscribed;
|
||||
reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending &&
|
||||
!SteamManager.Workshop.IsInstalling(workshopItem);
|
||||
reinstallSprite.Color = reinstallButton.Enabled
|
||||
? reinstallSprite.Style.Color
|
||||
: Color.DimGray;
|
||||
});
|
||||
CreateSubscribeButton(workshopItem,
|
||||
rightSideButtonRectT(),
|
||||
spriteScale: 0.8f);
|
||||
|
||||
var padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null);
|
||||
|
||||
var horizontalLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.45f), verticalLayout.RectTransform),
|
||||
isHorizontal: true)
|
||||
{
|
||||
Stretch = true
|
||||
};
|
||||
|
||||
TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) =>
|
||||
{
|
||||
authorButton.Text = author.Name;
|
||||
authorButton.RectTransform.NonScaledSize =
|
||||
((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z),
|
||||
authorButton.RectTransform.NonScaledSize.Y);
|
||||
});
|
||||
|
||||
var thumbnailSuperContainer = new GUIFrame(
|
||||
new RectTransform(Vector2.One, horizontalLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight),
|
||||
style: null);
|
||||
GUIFrame thumbnailContainer = CreateThumbnailContainer(thumbnailSuperContainer, Vector2.One,
|
||||
scaleBasis: ScaleBasis.BothHeight);
|
||||
CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer);
|
||||
thumbnailContainer.RectTransform.Anchor = Anchor.Center;
|
||||
thumbnailContainer.RectTransform.Pivot = Pivot.Center;
|
||||
|
||||
var statsBox = new GUIFrame(new RectTransform((0.6f, 1.0f), horizontalLayout.RectTransform),
|
||||
style: "GUIFrameListBox");
|
||||
|
||||
#region Stats box
|
||||
var statsHorizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, statsBox.RectTransform), isHorizontal: true);
|
||||
var statsVertical0
|
||||
= new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform));
|
||||
|
||||
statFrame("", ""); //padding
|
||||
|
||||
var scoreFrame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), style: null);
|
||||
var scoreLabel = new GUITextBlock(new RectTransform((0.4f, 1.0f), scoreFrame.RectTransform),
|
||||
TextManager.Get("WorkshopItemScore"), font: GUIStyle.SubHeadingFont);
|
||||
var scoreStarContainer
|
||||
= new GUILayoutGroup(
|
||||
new RectTransform((0.6f, 1.0f), scoreFrame.RectTransform, Anchor.CenterRight),
|
||||
isHorizontal: true,
|
||||
childAnchor: Anchor.CenterLeft) { Stretch = true };
|
||||
var starColor = Color.Lerp(
|
||||
Color.Lerp(Color.Red, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)),
|
||||
Color.Lime, Math.Max(0.0f, (workshopItem.Score - 0.5f) * 2.0f));
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
bool isStarLit = i <= Round(workshopItem.Score * 5.0f);
|
||||
var star = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
|
||||
style: isStarLit ? "GUIStarIconBright" : "GUIStarIconDark");
|
||||
if (isStarLit)
|
||||
{
|
||||
star.Color = starColor;
|
||||
star.HoverColor = starColor;
|
||||
star.SelectedColor = starColor;
|
||||
}
|
||||
}
|
||||
var scoreVoteCountPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
|
||||
style: null);
|
||||
var scoreVoteCount = new GUITextBlock(
|
||||
new RectTransform(Vector2.One, scoreStarContainer.RectTransform),
|
||||
TextManager.GetWithVariable("WorkshopItemVotes", "[VoteCount]",
|
||||
(workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.CenterLeft)
|
||||
{
|
||||
Padding = Vector4.Zero
|
||||
};
|
||||
|
||||
void statFrame(LocalizedString labelText, LocalizedString dataText)
|
||||
{
|
||||
var frame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0!.RectTransform), style: null);
|
||||
var label = new GUITextBlock(new RectTransform((0.4f, 1.0f), frame.RectTransform),
|
||||
labelText, font: GUIStyle.SubHeadingFont);
|
||||
var data = new GUITextBlock(new RectTransform((0.6f, 1.0f), frame.RectTransform, Anchor.CenterRight),
|
||||
dataText, font: GUIStyle.Font)
|
||||
{
|
||||
Padding = Vector4.Zero
|
||||
};
|
||||
}
|
||||
|
||||
statFrame(TextManager.Get("WorkshopItemFileSize"), MathUtils.GetBytesReadable(workshopItem.SizeOfFileInBytes));
|
||||
statFrame(TextManager.Get("WorkshopItemCreationDate"), workshopItem.Created.ToShortDateString());
|
||||
statFrame(TextManager.Get("WorkshopItemModificationDate"), workshopItem.Updated.ToShortDateString());
|
||||
|
||||
var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform),
|
||||
TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont);
|
||||
CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((1.0f, 0.3f), statsVertical0.RectTransform), canBeFocused: false);
|
||||
#endregion
|
||||
|
||||
var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform));
|
||||
CreateBBCodeElement(workshopItem.Description, descriptionListBox);
|
||||
|
||||
var showInSteamContainer
|
||||
= new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null);
|
||||
CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), showInSteamContainer.RectTransform, Anchor.CenterRight));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
static partial class SteamManager
|
||||
{
|
||||
private enum LobbyState
|
||||
{
|
||||
NotConnected,
|
||||
Creating,
|
||||
Owner,
|
||||
Joining,
|
||||
Joined
|
||||
}
|
||||
private static UInt64 lobbyID = 0;
|
||||
private static LobbyState lobbyState = LobbyState.NotConnected;
|
||||
private static Steamworks.Data.Lobby? currentLobby;
|
||||
public static UInt64 CurrentLobbyID
|
||||
{
|
||||
get { return currentLobby?.Id ?? 0; }
|
||||
}
|
||||
|
||||
public static void CreateLobby(ServerSettings serverSettings)
|
||||
{
|
||||
if (lobbyState != LobbyState.NotConnected) { return; }
|
||||
lobbyState = LobbyState.Creating;
|
||||
TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10),
|
||||
(lobby) =>
|
||||
{
|
||||
if (lobbyState != LobbyState.Creating)
|
||||
{
|
||||
LeaveLobby();
|
||||
return;
|
||||
}
|
||||
|
||||
currentLobby = ((Task<Steamworks.Data.Lobby?>)lobby).Result;
|
||||
|
||||
if (currentLobby == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to create Steam lobby");
|
||||
lobbyState = LobbyState.NotConnected;
|
||||
return;
|
||||
}
|
||||
|
||||
DebugConsole.NewMessage("Lobby created!", Microsoft.Xna.Framework.Color.Lime);
|
||||
|
||||
lobbyState = LobbyState.Owner;
|
||||
lobbyID = (currentLobby?.Id).Value;
|
||||
|
||||
if (serverSettings.IsPublic)
|
||||
{
|
||||
currentLobby?.SetPublic();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLobby?.SetFriendsOnly();
|
||||
}
|
||||
currentLobby?.SetJoinable(true);
|
||||
|
||||
UpdateLobby(serverSettings);
|
||||
});
|
||||
}
|
||||
|
||||
public static void UpdateLobby(ServerSettings serverSettings)
|
||||
{
|
||||
if (GameMain.Client == null)
|
||||
{
|
||||
LeaveLobby();
|
||||
}
|
||||
|
||||
if (lobbyState == LobbyState.NotConnected)
|
||||
{
|
||||
CreateLobby(serverSettings);
|
||||
}
|
||||
|
||||
if (lobbyState != LobbyState.Owner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent);
|
||||
|
||||
currentLobby?.SetData("name", serverSettings.ServerName);
|
||||
currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString());
|
||||
currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString());
|
||||
//currentLobby?.SetData("hostipaddress", lobbyIP);
|
||||
string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString();
|
||||
currentLobby?.SetData("pinglocation", pingLocation ?? "");
|
||||
currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID()));
|
||||
currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString());
|
||||
|
||||
currentLobby?.SetData("message", serverSettings.ServerMessageText);
|
||||
currentLobby?.SetData("version", GameMain.Version.ToString());
|
||||
|
||||
currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
|
||||
currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation)));
|
||||
currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId)));
|
||||
currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString());
|
||||
currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString());
|
||||
currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString());
|
||||
currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString());
|
||||
currentLobby?.SetData("allowspectating", serverSettings.AllowSpectating.ToString());
|
||||
currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString());
|
||||
currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString());
|
||||
currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString());
|
||||
currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString());
|
||||
currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString());
|
||||
currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString());
|
||||
currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? "");
|
||||
|
||||
DebugConsole.Log("Lobby updated!");
|
||||
}
|
||||
|
||||
public static void LeaveLobby()
|
||||
{
|
||||
if (lobbyState != LobbyState.NotConnected)
|
||||
{
|
||||
currentLobby?.Leave(); currentLobby = null;
|
||||
lobbyState = LobbyState.NotConnected;
|
||||
|
||||
lobbyID = 0;
|
||||
|
||||
Steamworks.SteamMatchmaking.ResetActions();
|
||||
}
|
||||
}
|
||||
public static void JoinLobby(UInt64 id, bool joinServer)
|
||||
{
|
||||
if (currentLobby.HasValue && currentLobby.Value.Id == id) { return; }
|
||||
if (lobbyID == id) { return; }
|
||||
lobbyState = LobbyState.Joining;
|
||||
lobbyID = id;
|
||||
|
||||
TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID),
|
||||
(lobby) =>
|
||||
{
|
||||
currentLobby = ((Task<Steamworks.Data.Lobby?>)lobby).Result;
|
||||
lobbyState = LobbyState.Joined;
|
||||
lobbyID = (currentLobby?.Id).Value;
|
||||
if (joinServer)
|
||||
{
|
||||
GameMain.Instance.ConnectLobby = 0;
|
||||
GameMain.Instance.ConnectName = currentLobby?.GetData("servername");
|
||||
GameMain.Instance.ConnectEndpoint = SteamIDUInt64ToString((currentLobby?.Owner.Id).Value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static bool GetServers(Action<ServerInfo> addToServerList, Action serverQueryFinished)
|
||||
{
|
||||
if (!IsInitialized) { return false; }
|
||||
|
||||
int doneTasks = 0;
|
||||
void taskDone()
|
||||
{
|
||||
doneTasks++;
|
||||
if (doneTasks >= 2)
|
||||
{
|
||||
serverQueryFinished?.Invoke();
|
||||
serverQueryFinished = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) =>
|
||||
{
|
||||
DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow);
|
||||
};
|
||||
|
||||
TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(),
|
||||
(t) =>
|
||||
{
|
||||
Steamworks.Dispatch.OnDebugCallback = null;
|
||||
if (t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies");
|
||||
taskDone();
|
||||
return;
|
||||
}
|
||||
var lobbies = ((Task<List<Steamworks.Data.Lobby>>)t).Result;
|
||||
if (lobbies != null)
|
||||
{
|
||||
foreach (var lobby in lobbies)
|
||||
{
|
||||
if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; }
|
||||
|
||||
ServerInfo serverInfo = new ServerInfo();
|
||||
serverInfo.ServerName = lobby.GetData("name");
|
||||
serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner"));
|
||||
serverInfo.LobbyID = lobby.Id;
|
||||
bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword);
|
||||
serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0;
|
||||
serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1;
|
||||
serverInfo.RespondedToSteamQuery = true;
|
||||
|
||||
AssignLobbyDataToServerInfo(lobby, serverInfo);
|
||||
|
||||
addToServerList(serverInfo);
|
||||
}
|
||||
}
|
||||
taskDone();
|
||||
});
|
||||
|
||||
Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet();
|
||||
void onServer(Steamworks.Data.ServerInfo info, bool responsive)
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Name)) { return; }
|
||||
|
||||
ServerInfo serverInfo = new ServerInfo
|
||||
{
|
||||
ServerName = info.Name,
|
||||
HasPassword = info.Passworded,
|
||||
IP = info.Address.ToString(),
|
||||
Port = info.ConnectionPort.ToString(),
|
||||
PlayerCount = info.Players,
|
||||
MaxPlayers = info.MaxPlayers,
|
||||
RespondedToSteamQuery = responsive
|
||||
};
|
||||
|
||||
if (responsive)
|
||||
{
|
||||
TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(),
|
||||
(t) =>
|
||||
{
|
||||
if (t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var rules = ((Task<Dictionary<string, string>>)t).Result;
|
||||
AssignServerRulesToServerInfo(rules, serverInfo);
|
||||
|
||||
CrossThread.RequestExecutionOnMainThread(() =>
|
||||
{
|
||||
addToServerList(serverInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
CrossThread.RequestExecutionOnMainThread(() =>
|
||||
{
|
||||
addToServerList(serverInfo);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
serverQuery.OnResponsiveServer += (info) => onServer(info, true);
|
||||
serverQuery.OnUnresponsiveServer += (info) => onServer(info, false);
|
||||
|
||||
TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(),
|
||||
(t) =>
|
||||
{
|
||||
serverQuery.Dispose();
|
||||
taskDone();
|
||||
if (t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<List<Steamworks.Data.Lobby>> LobbyQueryRequest()
|
||||
{
|
||||
List<Steamworks.Data.Lobby> allLobbies = new List<Steamworks.Data.Lobby>();
|
||||
Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery()
|
||||
.FilterDistanceWorldwide()
|
||||
.WithMaxResults(50);
|
||||
//steamworks seems to unable to retrieve more than 50
|
||||
//lobbies per request; to work around this, we'll make
|
||||
//up to 10 requests, asking to ignore all previous results
|
||||
//in each subsequent request
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync();
|
||||
if (lobbies == null) { break; }
|
||||
foreach (var l in lobbies)
|
||||
{
|
||||
lobbyQuery = lobbyQuery
|
||||
.WithoutKeyValue("lobbyowner", l.GetData("lobbyowner"));
|
||||
}
|
||||
allLobbies.AddRange(lobbies);
|
||||
}
|
||||
|
||||
//make sure all returned lobbies are distinct, don't want any duplicates here
|
||||
return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList();
|
||||
}
|
||||
|
||||
public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo)
|
||||
{
|
||||
serverInfo.OwnerVerified = true;
|
||||
|
||||
serverInfo.ServerMessage = lobby.GetData("message");
|
||||
serverInfo.GameVersion = lobby.GetData("version");
|
||||
|
||||
serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(','));
|
||||
serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(','));
|
||||
|
||||
string workshopIdData = lobby.GetData("contentpackageid");
|
||||
if (!string.IsNullOrEmpty(workshopIdData))
|
||||
{
|
||||
serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData));
|
||||
}
|
||||
else
|
||||
{
|
||||
string[] workshopUrls = lobby.GetData("contentpackageurl").Split(',');
|
||||
serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls));
|
||||
}
|
||||
|
||||
serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist");
|
||||
if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; }
|
||||
if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; }
|
||||
|
||||
serverInfo.AllowSpectating = getLobbyBool("allowspectating");
|
||||
serverInfo.AllowRespawn = getLobbyBool("allowrespawn");
|
||||
serverInfo.VoipEnabled = getLobbyBool("voicechatenabled");
|
||||
serverInfo.KarmaEnabled = getLobbyBool("karmaenabled");
|
||||
serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled");
|
||||
if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; }
|
||||
|
||||
serverInfo.GameStarted = lobby.GetData("gamestarted") == "True";
|
||||
serverInfo.GameMode = (lobby.GetData("gamemode") ?? "").ToIdentifier();
|
||||
if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle;
|
||||
|
||||
if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count ||
|
||||
serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count)
|
||||
{
|
||||
//invalid contentpackage info
|
||||
serverInfo.ContentPackageNames.Clear();
|
||||
serverInfo.ContentPackageHashes.Clear();
|
||||
serverInfo.ContentPackageWorkshopIds.Clear();
|
||||
}
|
||||
|
||||
string pingLocation = lobby.GetData("pinglocation");
|
||||
if (!string.IsNullOrEmpty(pingLocation))
|
||||
{
|
||||
serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation);
|
||||
}
|
||||
|
||||
bool? getLobbyBool(string key)
|
||||
{
|
||||
string data = lobby.GetData(key);
|
||||
if (string.IsNullOrEmpty(data)) { return null; }
|
||||
return data == "True" || data == "true";
|
||||
}
|
||||
}
|
||||
|
||||
public static void AssignServerRulesToServerInfo(Dictionary<string, string> rules, ServerInfo serverInfo)
|
||||
{
|
||||
serverInfo.OwnerVerified = true;
|
||||
|
||||
if (rules == null) { return; }
|
||||
|
||||
if (rules.ContainsKey("message")) serverInfo.ServerMessage = rules["message"];
|
||||
if (rules.ContainsKey("version")) serverInfo.GameVersion = rules["version"];
|
||||
|
||||
if (rules.ContainsKey("playercount"))
|
||||
{
|
||||
if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount;
|
||||
}
|
||||
|
||||
serverInfo.ContentPackageNames.Clear();
|
||||
serverInfo.ContentPackageHashes.Clear();
|
||||
serverInfo.ContentPackageWorkshopIds.Clear();
|
||||
if (rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(','));
|
||||
if (rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(','));
|
||||
if (rules.ContainsKey("contentpackageid"))
|
||||
{
|
||||
serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"]));
|
||||
}
|
||||
else if (rules.ContainsKey("contentpackageurl"))
|
||||
{
|
||||
string[] workshopUrls = rules["contentpackageurl"].Split(',');
|
||||
serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls));
|
||||
}
|
||||
|
||||
if (rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True";
|
||||
if (rules.ContainsKey("modeselectionmode"))
|
||||
{
|
||||
if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode;
|
||||
}
|
||||
if (rules.ContainsKey("subselectionmode"))
|
||||
{
|
||||
if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode;
|
||||
}
|
||||
if (rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = rules["allowspectating"] == "True";
|
||||
if (rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = rules["allowrespawn"] == "True";
|
||||
if (rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = rules["voicechatenabled"] == "True";
|
||||
if (rules.ContainsKey("traitors"))
|
||||
{
|
||||
if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled;
|
||||
}
|
||||
|
||||
if (rules.ContainsKey("gamestarted")) serverInfo.GameStarted = rules["gamestarted"] == "True";
|
||||
if (rules.ContainsKey("gamemode"))
|
||||
{
|
||||
serverInfo.GameMode = rules["gamemode"].ToIdentifier();
|
||||
}
|
||||
if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle))
|
||||
{
|
||||
serverInfo.PlayStyle = playStyle;
|
||||
}
|
||||
|
||||
if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count ||
|
||||
serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count)
|
||||
{
|
||||
//invalid contentpackage info
|
||||
serverInfo.ContentPackageNames.Clear();
|
||||
serverInfo.ContentPackageHashes.Clear();
|
||||
serverInfo.ContentPackageWorkshopIds.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Directory = Barotrauma.IO.Directory;
|
||||
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
|
||||
using Path = Barotrauma.IO.Path;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
public partial class WorkshopMenu
|
||||
{
|
||||
private class LocalThumbnail : IDisposable
|
||||
{
|
||||
public Texture2D? Texture { get; private set; } = null;
|
||||
public bool Loading = true;
|
||||
|
||||
public LocalThumbnail(string path)
|
||||
{
|
||||
TaskPool.Add($"LocalThumbnail {path}",
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Yield();
|
||||
return TextureLoader.FromFile(path, compress: false, mipmap: false);
|
||||
}),
|
||||
(t) =>
|
||||
{
|
||||
Loading = false;
|
||||
Task<Texture2D?> texTask = (t as Task<Texture2D?>)!;
|
||||
if (disposed)
|
||||
{
|
||||
texTask.Result?.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Texture = texTask.Result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
~LocalThumbnail()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) { return; }
|
||||
|
||||
disposed = true;
|
||||
Texture?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private LocalThumbnail? localThumbnail = null;
|
||||
|
||||
private void CreateLocalThumbnail(string path, GUIFrame thumbnailContainer)
|
||||
{
|
||||
thumbnailContainer.ClearChildren();
|
||||
localThumbnail?.Dispose();
|
||||
localThumbnail = new LocalThumbnail(path);
|
||||
CreateAsyncThumbnailComponent(thumbnailContainer, () => localThumbnail?.Texture, () => localThumbnail is { Loading: true });
|
||||
}
|
||||
|
||||
private static async Task<(int FileCount, int ByteCount)> GetModDirInfo(string dir, GUITextBlock label)
|
||||
{
|
||||
int fileCount = 0;
|
||||
int byteCount = 0;
|
||||
|
||||
var files = Directory.GetFiles(dir, pattern: "*", option: System.IO.SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
await Task.Yield();
|
||||
fileCount++;
|
||||
byteCount += (int)(new Barotrauma.IO.FileInfo(file).Length);
|
||||
label.Text = TextManager.GetWithVariables(
|
||||
"ModDirInfo",
|
||||
("[filecount]", fileCount.ToString(CultureInfo.InvariantCulture)),
|
||||
("[size]", MathUtils.GetBytesReadable(byteCount)));
|
||||
}
|
||||
|
||||
return (fileCount, byteCount);
|
||||
}
|
||||
|
||||
private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame)
|
||||
{
|
||||
ContentPackageManager.LocalPackages.Refresh();
|
||||
ContentPackageManager.WorkshopPackages.Refresh();
|
||||
|
||||
var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect");
|
||||
Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action }
|
||||
? action
|
||||
: null;
|
||||
|
||||
void deselectItem()
|
||||
{
|
||||
deselectAction?.Invoke();
|
||||
SelectTab(Tab.Publish);
|
||||
}
|
||||
|
||||
parentFrame.ClearChildren();
|
||||
GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform),
|
||||
childAnchor: Anchor.TopCenter);
|
||||
|
||||
Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default;
|
||||
ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package)
|
||||
? package
|
||||
: ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
|
||||
ContentPackage? workshopPackage
|
||||
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
|
||||
if (localPackage is null)
|
||||
{
|
||||
new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null);
|
||||
|
||||
//Local copy does not exist; check for Workshop copy
|
||||
bool workshopCopyExists =
|
||||
ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id);
|
||||
|
||||
new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform),
|
||||
TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"),
|
||||
wrap: true);
|
||||
|
||||
var buttonLayout = new GUILayoutGroup(new RectTransform((0.6f, 0.1f), mainLayout.RectTransform),
|
||||
isHorizontal: true);
|
||||
var yesButton = new GUIButton(new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform),
|
||||
text: TextManager.Get("Yes"))
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
CoroutineManager.StartCoroutine(MessageBoxCoroutine((currentStepText, messageBox)
|
||||
=> CreateLocalCopy(currentStepText, workshopItem, parentFrame)),
|
||||
$"CreateLocalCopy {workshopItem.Id}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var noButton = new GUIButton(new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform),
|
||||
text: TextManager.Get("No"))
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
deselectItem();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ContentPackageManager.LocalPackages.Contains(localPackage))
|
||||
{
|
||||
throw new Exception($"Content package \"{localPackage.Name}\" is not a local package!");
|
||||
}
|
||||
|
||||
var selectedTitle =
|
||||
new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), workshopItem.Title ?? localPackage.Name,
|
||||
font: GUIStyle.LargeFont);
|
||||
if (workshopItem.Id != 0)
|
||||
{
|
||||
var showInSteamButton = CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), selectedTitle.RectTransform, Anchor.CenterRight));
|
||||
}
|
||||
|
||||
Spacer(mainLayout, height: 0.03f);
|
||||
|
||||
var (leftTop, _, rightTop)
|
||||
= CreateSidebars(mainLayout, leftWidth: 0.2f, centerWidth: 0.01f, rightWidth: 0.79f,
|
||||
height: 0.4f);
|
||||
leftTop.Stretch = true;
|
||||
rightTop.Stretch = true;
|
||||
|
||||
Label(leftTop, TextManager.Get("WorkshopItemPreviewImage"), GUIStyle.SubHeadingFont);
|
||||
string? thumbnailPath = null;
|
||||
var thumbnailContainer = CreateThumbnailContainer(leftTop, Vector2.One, ScaleBasis.BothWidth);
|
||||
if (workshopItem.Id != 0)
|
||||
{
|
||||
CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer);
|
||||
}
|
||||
|
||||
var browseThumbnail =
|
||||
new GUIButton(NewItemRectT(leftTop),
|
||||
TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall")
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
FileSelection.ClearFileTypeFilters();
|
||||
FileSelection.AddFileTypeFilter("PNG", "*.png");
|
||||
FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg");
|
||||
FileSelection.AddFileTypeFilter("All files", "*.*");
|
||||
FileSelection.SelectFileTypeFilter("*.png");
|
||||
FileSelection.CurrentDirectory
|
||||
= Path.GetFullPath(Path.GetDirectoryName(localPackage.Path)!);
|
||||
|
||||
FileSelection.OnFileSelected = (fn) =>
|
||||
{
|
||||
thumbnailPath = fn;
|
||||
CreateLocalThumbnail(thumbnailPath, thumbnailContainer);
|
||||
};
|
||||
|
||||
FileSelection.Open = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont);
|
||||
var titleTextBox = new GUITextBox(NewItemRectT(rightTop), workshopItem.Title ?? localPackage.Name);
|
||||
|
||||
Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont);
|
||||
var descriptionTextBox
|
||||
= ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty);
|
||||
|
||||
var (leftBottom, _, rightBottom)
|
||||
= CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f);
|
||||
leftBottom.Stretch = true;
|
||||
rightBottom.Stretch = true;
|
||||
|
||||
Label(leftBottom, TextManager.Get("WorkshopItemVersion"), GUIStyle.SubHeadingFont);
|
||||
var modVersion = localPackage.ModVersion;
|
||||
if (workshopPackage is { ModVersion: { } workshopVersion } &&
|
||||
modVersion.Equals(workshopVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
modVersion = ModProject.IncrementModVersion(modVersion);
|
||||
}
|
||||
|
||||
char[] forbiddenVersionCharacters = { ';', '=' };
|
||||
var versionTextBox = new GUITextBox(NewItemRectT(leftBottom), modVersion);
|
||||
versionTextBox.OnTextChanged += (box, text) =>
|
||||
{
|
||||
if (text.Any(c => forbiddenVersionCharacters.Contains(c)))
|
||||
{
|
||||
foreach (var c in forbiddenVersionCharacters)
|
||||
{
|
||||
text = text.Replace($"{c}", "");
|
||||
}
|
||||
|
||||
box.Text = text;
|
||||
box.Flash(GUIStyle.Red);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Label(leftBottom, TextManager.Get("WorkshopItemChangeNote"), GUIStyle.SubHeadingFont);
|
||||
var changeNoteTextBox = ScrollableTextBox(leftBottom, 5.0f, "");
|
||||
|
||||
Label(rightBottom, TextManager.Get("WorkshopItemTags"), GUIStyle.SubHeadingFont);
|
||||
var tagsList = CreateTagsList(SteamManager.Workshop.Tags, NewItemRectT(rightBottom, heightScale: 4.0f),
|
||||
canBeFocused: true);
|
||||
Dictionary<Identifier, GUIButton> tagButtons = tagsList.Content.Children.Cast<GUIButton>()
|
||||
.Select(b => ((Identifier)b.UserData, b)).ToDictionary();
|
||||
if (workshopItem.Tags != null)
|
||||
{
|
||||
foreach (Identifier tag in workshopItem.Tags.ToIdentifiers())
|
||||
{
|
||||
if (tagButtons.TryGetValue(tag, out var button)) { button.Selected = true; }
|
||||
}
|
||||
}
|
||||
|
||||
GUILayoutGroup visibilityLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true);
|
||||
|
||||
var visibilityLabel = Label(visibilityLayout, TextManager.Get("WorkshopItemVisibility"), GUIStyle.SubHeadingFont);
|
||||
visibilityLabel.RectTransform.RelativeSize = (0.6f, 1.0f);
|
||||
visibilityLabel.TextAlignment = Alignment.CenterRight;
|
||||
|
||||
Steamworks.Ugc.Visibility visibility = workshopItem.Visibility;
|
||||
var visibilityDropdown = DropdownEnum(
|
||||
visibilityLayout,
|
||||
(v) => TextManager.Get($"WorkshopItemVisibility.{v}"),
|
||||
visibility,
|
||||
(v) => visibility = v);
|
||||
visibilityDropdown.RectTransform.RelativeSize = (0.4f, 1.0f);
|
||||
|
||||
var fileInfoLabel = Label(rightBottom, "", GUIStyle.Font, heightScale: 1.0f);
|
||||
fileInfoLabel.TextAlignment = Alignment.CenterRight;
|
||||
TaskPool.Add($"FileInfoLabel{workshopItem.Id}", GetModDirInfo(localPackage.Dir, fileInfoLabel), t => { });
|
||||
|
||||
GUILayoutGroup buttonLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true, childAnchor: Anchor.CenterRight);
|
||||
|
||||
RectTransform newButtonRectT()
|
||||
=> new RectTransform((0.4f, 1.0f), buttonLayout.RectTransform);
|
||||
|
||||
var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemPublish"))
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
//Reload the package to force hash recalculation
|
||||
string packageName = localPackage.Name;
|
||||
localPackage = ContentPackageManager.ReloadContentPackage(localPackage);
|
||||
if (localPackage is null)
|
||||
{
|
||||
throw new Exception($"\"{packageName}\" was removed upon reload");
|
||||
}
|
||||
|
||||
//Set up the Ugc.Editor object that we'll need to publish
|
||||
Steamworks.Ugc.Editor ugcEditor =
|
||||
workshopItem.Id == 0
|
||||
? Steamworks.Ugc.Editor.NewCommunityFile
|
||||
: new Steamworks.Ugc.Editor(workshopItem.Id);
|
||||
ugcEditor = ugcEditor.WithTitle(titleTextBox.Text)
|
||||
.WithDescription(descriptionTextBox.Text)
|
||||
.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);
|
||||
|
||||
CoroutineManager.StartCoroutine(
|
||||
MessageBoxCoroutine((currentStepText, messageBox)
|
||||
=> PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage)));
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (workshopItem.Id != 0)
|
||||
{
|
||||
var deleteItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemDelete"), color: GUIStyle.Red)
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
var confirmDeletion = new GUIMessageBox(
|
||||
headerText: TextManager.Get("WorkshopItemDelete"),
|
||||
text: TextManager.GetWithVariable("WorkshopItemDeleteVerification", "[itemname]", workshopItem.Title!),
|
||||
buttons: new[] { TextManager.Get("Yes"), TextManager.Get("No") });
|
||||
confirmDeletion.Buttons[0].OnClicked = (yesBuffer, o1) =>
|
||||
{
|
||||
TaskPool.Add($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id),
|
||||
t =>
|
||||
{
|
||||
confirmDeletion.Close();
|
||||
deselectItem();
|
||||
});
|
||||
return false;
|
||||
};
|
||||
confirmDeletion.Buttons[1].OnClicked = (noButton, o1) =>
|
||||
{
|
||||
confirmDeletion.Close();
|
||||
return false;
|
||||
};
|
||||
|
||||
return false;
|
||||
},
|
||||
HoverColor = Color.Lerp(GUIStyle.Red, Color.White, 0.3f),
|
||||
PressedColor = Color.Lerp(GUIStyle.Red, Color.Black, 0.3f),
|
||||
};
|
||||
deleteItemButton.TextBlock.TextColor = Color.Black;
|
||||
deleteItemButton.TextBlock.HoverTextColor = Color.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<CoroutineStatus> MessageBoxCoroutine(Func<GUITextBlock, GUIMessageBox, IEnumerable<CoroutineStatus>> subcoroutine)
|
||||
{
|
||||
var messageBox = new GUIMessageBox("", "", relativeSize: (0.4f, 0.4f), buttons: new [] { TextManager.Get("Cancel") });
|
||||
messageBox.Buttons[0].OnClicked = (button, o) =>
|
||||
{
|
||||
messageBox.Close();
|
||||
return false;
|
||||
};
|
||||
|
||||
var currentStepText = new GUITextBlock(new RectTransform((1.0f, 0.8f), messageBox.InnerFrame.RectTransform),
|
||||
"...", font: GUIStyle.Font)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
|
||||
foreach (var status in subcoroutine(currentStepText, messageBox))
|
||||
{
|
||||
if (messageBox.Closed)
|
||||
{
|
||||
yield return CoroutineStatus.Success;
|
||||
yield break;
|
||||
}
|
||||
else if (status == CoroutineStatus.Failure || status == CoroutineStatus.Success)
|
||||
{
|
||||
messageBox.Close();
|
||||
yield return status;
|
||||
yield break;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<CoroutineStatus> CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame)
|
||||
{
|
||||
ContentPackage? workshopCopy =
|
||||
ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
|
||||
if (workshopCopy is null)
|
||||
{
|
||||
if (!SteamManager.Workshop.CanBeInstalled(workshopItem))
|
||||
{
|
||||
//Must download!
|
||||
while (!SteamManager.Workshop.CanBeInstalled(workshopItem))
|
||||
{
|
||||
bool shouldForceInstall = workshopItem.IsInstalled
|
||||
&& Directory.Exists(workshopItem.Directory)
|
||||
&& !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem);
|
||||
shouldForceInstall |= workshopItem is
|
||||
{ IsDownloading: false, IsDownloadPending: false, IsInstalled: false };
|
||||
if (shouldForceInstall)
|
||||
{
|
||||
SteamManager.Workshop.ForceRedownload(workshopItem);
|
||||
}
|
||||
currentStepText.Text = $"Downloading {Percentage(workshopItem.DownloadAmount)}";
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem);
|
||||
}
|
||||
TaskPool.Add($"Install {workshopItem.Title}",
|
||||
SteamManager.Workshop.WaitForInstall(workshopItem),
|
||||
(t) =>
|
||||
{
|
||||
ContentPackageManager.WorkshopPackages.Refresh();
|
||||
});
|
||||
while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id))
|
||||
{
|
||||
currentStepText.Text = $"Installing";
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
|
||||
workshopCopy =
|
||||
ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id);
|
||||
}
|
||||
|
||||
bool localCopyMade = false;
|
||||
TaskPool.Add($"Create local copy {workshopItem.Title}",
|
||||
SteamManager.Workshop.CreateLocalCopy(workshopCopy),
|
||||
(t) =>
|
||||
{
|
||||
ContentPackageManager.LocalPackages.Refresh();
|
||||
localCopyMade = true;
|
||||
});
|
||||
while (!localCopyMade)
|
||||
{
|
||||
currentStepText.Text = $"Creating local copy";
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
|
||||
PopulatePublishTab(workshopItem, parentFrame);
|
||||
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
private IEnumerable<CoroutineStatus> PublishItem(
|
||||
GUITextBlock currentStepText, GUIMessageBox messageBox,
|
||||
string modVersion, Steamworks.Ugc.Editor editor, ContentPackage localPackage)
|
||||
{
|
||||
bool stagingReady = false;
|
||||
TaskPool.Add("CreatePublishStagingCopy",
|
||||
SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage),
|
||||
(t) =>
|
||||
{
|
||||
Exception? exception = t.Exception?.InnerException ?? t.Exception;
|
||||
if (exception != null)
|
||||
{
|
||||
throw new Exception($"Failed to create staging copy: {exception.Message} {exception.StackTrace}");
|
||||
}
|
||||
stagingReady = true;
|
||||
});
|
||||
currentStepText.Text = "Copying item to staging folder...";
|
||||
while (!stagingReady) { yield return new WaitForSeconds(0.5f); }
|
||||
|
||||
editor = editor
|
||||
.WithContent(SteamManager.Workshop.PublishStagingDir)
|
||||
.ForAppId(SteamManager.AppID);
|
||||
|
||||
messageBox.Buttons[0].Enabled = false;
|
||||
Steamworks.Ugc.PublishResult? result = null;
|
||||
TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})",
|
||||
editor.SubmitAsync(),
|
||||
(t) =>
|
||||
{
|
||||
result = ((Task<Steamworks.Ugc.PublishResult>)t).Result;
|
||||
});
|
||||
currentStepText.Text = "Submitting item to the Workshop...";
|
||||
while (!result.HasValue) { yield return new WaitForSeconds(0.5f); }
|
||||
|
||||
if (result.Value.Success)
|
||||
{
|
||||
var resultId = result.Value.FileId;
|
||||
Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId);
|
||||
SteamManager.Workshop.ForceRedownload(resultItem);
|
||||
while (!resultItem.IsInstalled)
|
||||
{
|
||||
currentStepText.Text = $"Downloading {Percentage(resultItem.DownloadAmount)}";
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
|
||||
bool installed = false;
|
||||
TaskPool.Add(
|
||||
"InstallNewlyPublished",
|
||||
SteamManager.Workshop.WaitForInstall(resultItem),
|
||||
(t) =>
|
||||
{
|
||||
installed = true;
|
||||
});
|
||||
while (!installed)
|
||||
{
|
||||
currentStepText.Text = $"Installing";
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
|
||||
var localModProject = new ModProject(localPackage)
|
||||
{
|
||||
SteamWorkshopId = resultId
|
||||
};
|
||||
localModProject.Save(localPackage.Path);
|
||||
ContentPackageManager.ReloadContentPackage(localPackage);
|
||||
ContentPackageManager.WorkshopPackages.Refresh();
|
||||
|
||||
if (result.Value.NeedsWorkshopAgreement)
|
||||
{
|
||||
SteamManager.OverlayCustomURL(resultItem.Url);
|
||||
}
|
||||
}
|
||||
SteamManager.Workshop.DeletePublishStagingCopy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Networking;
|
||||
using RestSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Color = Microsoft.Xna.Framework.Color;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
static partial class SteamManager
|
||||
{
|
||||
private static readonly List<Identifier> initializationErrors = new List<Identifier>();
|
||||
public static IReadOnlyList<Identifier> InitializationErrors => initializationErrors;
|
||||
|
||||
private static void InitializeProjectSpecific()
|
||||
{
|
||||
if (IsInitialized) { return; }
|
||||
|
||||
try
|
||||
{
|
||||
Steamworks.SteamClient.Init(AppID, false);
|
||||
IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid;
|
||||
|
||||
if (IsInitialized)
|
||||
{
|
||||
DebugConsole.NewMessage(
|
||||
$"Logged in as {GetUsername()} (SteamID {SteamIDUInt64ToString(GetSteamID())})");
|
||||
|
||||
popularTags.Clear();
|
||||
int i = 0;
|
||||
foreach (KeyValuePair<string, int> commonness in tagCommonness)
|
||||
{
|
||||
popularTags.Insert(i, commonness.Key);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking;
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
IsInitialized = false;
|
||||
initializationErrors.Add("SteamDllNotFound".ToIdentifier());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("SteamManager initialization threw an exception", e);
|
||||
IsInitialized = false;
|
||||
initializationErrors.Add("SteamClientInitFailed".ToIdentifier());
|
||||
}
|
||||
|
||||
if (!IsInitialized)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); }
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Steamworks is completely insane so the following needs comments:
|
||||
|
||||
//This callback seems to take place when the item in question has not been downloaded recently
|
||||
Steamworks.SteamUGC.GlobalOnItemInstalled = id => Workshop.OnItemDownloadComplete(id);
|
||||
|
||||
//This callback seems to take place when the item has been downloaded recently and an update
|
||||
//or a redownload has taken place
|
||||
Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => Workshop.OnItemDownloadComplete(id);
|
||||
|
||||
//Maybe I'm completely wrong! All I know is that we need to handle both!
|
||||
}
|
||||
}
|
||||
|
||||
public static bool NetworkingDebugLog { get; private set; } = false;
|
||||
|
||||
private static void LogSteamworksNetworking(Steamworks.NetDebugOutput nType, string pszMsg)
|
||||
{
|
||||
DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange);
|
||||
}
|
||||
|
||||
public static void SetSteamworksNetworkingDebugLog(bool enabled)
|
||||
{
|
||||
if (enabled == NetworkingDebugLog) { return; }
|
||||
if (enabled)
|
||||
{
|
||||
Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.Everything;
|
||||
}
|
||||
else
|
||||
{
|
||||
Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.None;
|
||||
}
|
||||
NetworkingDebugLog = enabled;
|
||||
}
|
||||
|
||||
public static async Task InitRelayNetworkAccess()
|
||||
{
|
||||
if (!IsInitialized) { return; }
|
||||
|
||||
await Task.Yield();
|
||||
Steamworks.SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
|
||||
//SetSteamworksNetworkingDebugLog(true);
|
||||
var status = Steamworks.SteamNetworkingUtils.Status;
|
||||
while (status.Avail != Steamworks.SteamNetworkingAvailability.Current)
|
||||
{
|
||||
if (status.Avail == Steamworks.SteamNetworkingAvailability.CannotTry ||
|
||||
status.Avail == Steamworks.SteamNetworkingAvailability.Previously ||
|
||||
status.Avail == Steamworks.SteamNetworkingAvailability.Failed)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to initialize Steamworks network relay: " +
|
||||
$"{Steamworks.SteamNetworkingUtils.Status.Avail}, " +
|
||||
$"{Steamworks.SteamNetworkingUtils.Status.AvailNetConfig}, " +
|
||||
$"{Steamworks.SteamNetworkingUtils.Status.Avail}, " +
|
||||
$"{Steamworks.SteamNetworkingUtils.Status.Msg}");
|
||||
break;
|
||||
}
|
||||
await Task.Delay(25);
|
||||
status = Steamworks.SteamNetworkingUtils.Status;
|
||||
}
|
||||
//SetSteamworksNetworkingDebugLog(false);
|
||||
}
|
||||
|
||||
|
||||
public static bool OverlayCustomURL(string url)
|
||||
{
|
||||
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Steamworks.SteamFriends.OpenWebOverlay(url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
public partial class WorkshopMenu
|
||||
{
|
||||
private static RectTransform NewItemRectT(GUILayoutGroup parent, float heightScale = 1.0f)
|
||||
=> new RectTransform((1.0f, 0.06f * heightScale), parent.RectTransform, Anchor.CenterLeft);
|
||||
|
||||
private static void Spacer(GUILayoutGroup parent, float height = 0.03f)
|
||||
{
|
||||
new GUIFrame(new RectTransform((1.0f, height), parent.RectTransform, Anchor.CenterLeft), style: null);
|
||||
}
|
||||
|
||||
private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font, float heightScale = 1.0f)
|
||||
{
|
||||
return new GUITextBlock(NewItemRectT(parent, heightScale), str, font: font);
|
||||
}
|
||||
|
||||
private static GUITextBox ScrollableTextBox(GUILayoutGroup parent, float heightScale, string text)
|
||||
{
|
||||
var containingListBox = new GUIListBox(NewItemRectT(parent, heightScale));
|
||||
var textBox = new GUITextBox(
|
||||
new RectTransform(Vector2.One, containingListBox.Content.RectTransform),
|
||||
"", style: "GUITextBoxNoBorder", wrap: true,
|
||||
textAlignment: Alignment.TopLeft);
|
||||
textBox.OnTextChanged += (textBox, text) =>
|
||||
{
|
||||
string wrappedText = textBox.TextBlock.WrappedText.Value;
|
||||
int measuredHeight = (int)textBox.Font.MeasureString(wrappedText).Y;
|
||||
textBox.RectTransform.NonScaledSize =
|
||||
(containingListBox.Content.Rect.Width,
|
||||
Math.Max(measuredHeight, containingListBox.Content.Rect.Height));
|
||||
containingListBox.UpdateScrollBarSize();
|
||||
|
||||
return true;
|
||||
};
|
||||
textBox.OnEnterPressed += (textBox, text) =>
|
||||
{
|
||||
string str = textBox.Text;
|
||||
int cursorPos = textBox.CaretIndex;
|
||||
textBox.Text = $"{str[..cursorPos]}\n{str[cursorPos..]}";
|
||||
textBox.CaretIndex = cursorPos + 1;
|
||||
|
||||
return true;
|
||||
};
|
||||
textBox.Text = text;
|
||||
return textBox;
|
||||
}
|
||||
|
||||
private static GUIDropDown DropdownEnum<T>(
|
||||
GUILayoutGroup parent, Func<T, LocalizedString> textFunc, T currentValue,
|
||||
Action<T> setter) where T : Enum
|
||||
=> Dropdown(parent, textFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter);
|
||||
|
||||
private static GUIDropDown Dropdown<T>(
|
||||
GUILayoutGroup parent, Func<T, LocalizedString> textFunc, IReadOnlyList<T> values, T currentValue,
|
||||
Action<T> setter, float heightScale = 1.0f)
|
||||
{
|
||||
var dropdown = new GUIDropDown(NewItemRectT(parent, heightScale));
|
||||
SwapDropdownValues(dropdown, textFunc, values, currentValue, setter);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
private static void SwapDropdownValues<T>(
|
||||
GUIDropDown dropdown, Func<T, LocalizedString> textFunc, IReadOnlyList<T> values, T currentValue,
|
||||
Action<T> setter)
|
||||
{
|
||||
if (dropdown.ListBox.Content.Children.Any(c => !(c.UserData is T)))
|
||||
{
|
||||
throw new Exception("SwapValues must preserve the type of the dropdown's userdata");
|
||||
}
|
||||
|
||||
dropdown.OnSelected = null;
|
||||
dropdown.ClearChildren();
|
||||
|
||||
values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v));
|
||||
dropdown.Select(values.IndexOf(currentValue));
|
||||
dropdown.OnSelected = (dd, obj) =>
|
||||
{
|
||||
setter((T)obj);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
private static int Round(float v) => (int)MathF.Round(v);
|
||||
private static string Percentage(float v) => $"{Round(v * 100)}%";
|
||||
|
||||
private struct ActionCarrier
|
||||
{
|
||||
public readonly Identifier Id;
|
||||
public readonly Action Action;
|
||||
public ActionCarrier(Identifier id, Action action)
|
||||
{
|
||||
Id = id;
|
||||
Action = action;
|
||||
}
|
||||
}
|
||||
|
||||
private GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action)
|
||||
=> new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null)
|
||||
{ UserData = new ActionCarrier(id, action) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
#nullable enable
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using RestSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.IO;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
static partial class SteamManager
|
||||
{
|
||||
public static partial class Workshop
|
||||
{
|
||||
public static readonly ImmutableArray<Identifier> Tags = new []
|
||||
{
|
||||
"submarine",
|
||||
"item",
|
||||
"monster",
|
||||
"art",
|
||||
"mission",
|
||||
"event set",
|
||||
"total conversion",
|
||||
"environment",
|
||||
"item assembly",
|
||||
"language",
|
||||
}.ToIdentifiers().ToImmutableArray();
|
||||
|
||||
public class ItemThumbnail : IDisposable
|
||||
{
|
||||
private struct RefCounter
|
||||
{
|
||||
internal bool Loading;
|
||||
internal Texture2D? Texture;
|
||||
internal int Count;
|
||||
}
|
||||
private readonly static Dictionary<UInt64, RefCounter> TextureRefs
|
||||
= new Dictionary<ulong, RefCounter>();
|
||||
|
||||
public UInt64 ItemId { get; private set; }
|
||||
public Texture2D? Texture
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (TextureRefs)
|
||||
{
|
||||
if (TextureRefs.TryGetValue(ItemId, out var refCounter))
|
||||
{
|
||||
return refCounter.Texture;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (TextureRefs)
|
||||
{
|
||||
if (TextureRefs.TryGetValue(ItemId, out var refCounter))
|
||||
{
|
||||
return refCounter.Loading;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken)
|
||||
{
|
||||
ItemId = item.Id;
|
||||
lock (TextureRefs)
|
||||
{
|
||||
if (TextureRefs.TryGetValue(ItemId, out var refCounter))
|
||||
{
|
||||
TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count + 1, Loading = refCounter.Loading };
|
||||
}
|
||||
else
|
||||
{
|
||||
TextureRefs[ItemId] = new RefCounter { Texture = null, Count = 1, Loading = true };
|
||||
TaskPool.Add($"Workshop thumbnail {item.Title}", GetTexture(item, cancellationToken), SaveTextureToRefCounter(item.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~ItemThumbnail()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (ItemId == 0) { return; }
|
||||
lock (TextureRefs)
|
||||
{
|
||||
var refCounter = TextureRefs[ItemId];
|
||||
TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count - 1 };
|
||||
if (TextureRefs[ItemId].Count <= 0)
|
||||
{
|
||||
TextureRefs[ItemId].Texture?.Dispose();
|
||||
TextureRefs.Remove(ItemId);
|
||||
}
|
||||
ItemId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Texture2D?> GetTexture(Steamworks.Ugc.Item item, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
string thumbnailUrl = item.PreviewImageUrl;
|
||||
if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; }
|
||||
var client = new RestClient(thumbnailUrl);
|
||||
var request = new RestRequest(".", Method.GET);
|
||||
IRestResponse response = await client.ExecuteTaskAsync(request, cancellationToken);
|
||||
if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed })
|
||||
{
|
||||
using var dataStream = new System.IO.MemoryStream();
|
||||
await dataStream.WriteAsync(response.RawBytes, cancellationToken);
|
||||
dataStream.Seek(0, System.IO.SeekOrigin.Begin);
|
||||
return TextureLoader.FromStream(dataStream, compress: false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Action<Task> SaveTextureToRefCounter(UInt64 itemId)
|
||||
=> (t) =>
|
||||
{
|
||||
if (t.IsCanceled) { return; }
|
||||
Texture2D? texture = ((Task<Texture2D?>)t).Result;
|
||||
lock (TextureRefs)
|
||||
{
|
||||
if (TextureRefs.TryGetValue(itemId, out var refCounter))
|
||||
{
|
||||
TextureRefs[itemId] = new RefCounter { Texture = texture, Count = refCounter.Count, Loading = false };
|
||||
}
|
||||
else if (texture != null)
|
||||
{
|
||||
texture.Dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public override int GetHashCode() => (int)ItemId;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is ItemThumbnail { ItemId: UInt64 otherId }
|
||||
&& otherId == ItemId;
|
||||
}
|
||||
|
||||
public const string PublishStagingDir = "WorkshopStaging";
|
||||
|
||||
public static void DeletePublishStagingCopy()
|
||||
{
|
||||
if (Directory.Exists(PublishStagingDir)) { Directory.Delete(PublishStagingDir, recursive: true); }
|
||||
}
|
||||
|
||||
private static void RefreshLocalMods()
|
||||
{
|
||||
CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh());
|
||||
}
|
||||
|
||||
public static async Task CreatePublishStagingCopy(string modVersion, ContentPackage contentPackage)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
if (!ContentPackageManager.LocalPackages.Contains(contentPackage))
|
||||
{
|
||||
throw new Exception("Expected local package");
|
||||
}
|
||||
|
||||
DeletePublishStagingCopy();
|
||||
Directory.CreateDirectory(PublishStagingDir);
|
||||
await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir);
|
||||
|
||||
//Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
|
||||
ModProject modProject = new ModProject(contentPackage);
|
||||
modProject.ModVersion = modVersion;
|
||||
modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName));
|
||||
}
|
||||
|
||||
public static async Task<ContentPackage?> CreateLocalCopy(ContentPackage contentPackage)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
if (!ContentPackageManager.WorkshopPackages.Contains(contentPackage))
|
||||
{
|
||||
throw new Exception("Expected Workshop package");
|
||||
}
|
||||
|
||||
if (contentPackage.SteamWorkshopId == 0)
|
||||
{
|
||||
throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}");
|
||||
}
|
||||
|
||||
string sanitizedName = ToolBox.RemoveInvalidFileNameChars(contentPackage.Name).Trim();
|
||||
if (sanitizedName.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new Exception($"Sanitized name for {contentPackage.Name} is empty");
|
||||
}
|
||||
|
||||
string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}";
|
||||
if (File.Exists(newPath) || Directory.Exists(newPath))
|
||||
{
|
||||
throw new Exception($"{newPath} already exists");
|
||||
}
|
||||
|
||||
await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath);
|
||||
|
||||
ModProject modProject = new ModProject(contentPackage);
|
||||
modProject.DiscardHashAndInstallTime();
|
||||
modProject.Save(Path.Combine(newPath, ContentPackage.FileListFileName));
|
||||
|
||||
RefreshLocalMods();
|
||||
|
||||
return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId);
|
||||
}
|
||||
|
||||
private struct InstallWaiter
|
||||
{
|
||||
private static readonly HashSet<ulong> waitingIds = new HashSet<ulong>();
|
||||
public ulong Id { get; private set; }
|
||||
|
||||
public InstallWaiter(ulong id)
|
||||
{
|
||||
Id = id;
|
||||
lock (waitingIds) { waitingIds.Add(Id); }
|
||||
}
|
||||
|
||||
public bool Waiting
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Id == 0) { return false; }
|
||||
|
||||
lock (waitingIds)
|
||||
{
|
||||
return waitingIds.Contains(Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void StopWaiting(ulong id)
|
||||
{
|
||||
lock (waitingIds)
|
||||
{
|
||||
waitingIds.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WaitForInstall(Steamworks.Ugc.Item item)
|
||||
=> await WaitForInstall(item.Id);
|
||||
|
||||
public static async Task WaitForInstall(ulong item)
|
||||
{
|
||||
var installWaiter = new InstallWaiter(item);
|
||||
while (installWaiter.Waiting) { await Task.Delay(500); }
|
||||
}
|
||||
|
||||
public static void OnItemDownloadComplete(ulong id, bool forceInstall = false)
|
||||
{
|
||||
if (!(Screen.Selected is MainMenuScreen) && !forceInstall)
|
||||
{
|
||||
if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id))
|
||||
{
|
||||
MainMenuScreen.WorkshopItemsToUpdate.Enqueue(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (CanBeInstalled(id)
|
||||
&& !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id))
|
||||
{
|
||||
TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
public partial class WorkshopMenu
|
||||
{
|
||||
public enum Tab
|
||||
{
|
||||
InstalledMods,
|
||||
//Overrides, //TODO: implement later
|
||||
PopularMods,
|
||||
Publish
|
||||
}
|
||||
|
||||
private GUILayoutGroup tabber;
|
||||
private Dictionary<Tab, (GUIButton Button, GUIFrame Content)> tabContents;
|
||||
|
||||
private GUIFrame contentFrame;
|
||||
|
||||
private CorePackage enabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected");
|
||||
|
||||
private readonly GUIDropDown enabledCoreDropdown;
|
||||
private readonly GUIListBox enabledRegularModsList;
|
||||
private readonly GUIListBox disabledRegularModsList;
|
||||
private readonly Action<ItemOrPackage> onInstalledInfoButtonHit;
|
||||
|
||||
private CancellationTokenSource taskCancelSrc = new CancellationTokenSource();
|
||||
private readonly HashSet<SteamManager.Workshop.ItemThumbnail> itemThumbnails = new HashSet<SteamManager.Workshop.ItemThumbnail>();
|
||||
|
||||
private readonly GUIListBox popularModsList;
|
||||
private readonly GUIListBox selfModsList;
|
||||
|
||||
public WorkshopMenu(GUIFrame parent)
|
||||
{
|
||||
var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false);
|
||||
|
||||
tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true };
|
||||
tabContents = new Dictionary<Tab, (GUIButton Button, GUIFrame Content)>();
|
||||
|
||||
contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null);
|
||||
|
||||
CreateInstalledModsTab(out enabledCoreDropdown, out enabledRegularModsList, out disabledRegularModsList, out onInstalledInfoButtonHit);
|
||||
CreatePopularModsTab(out popularModsList);
|
||||
CreatePublishTab(out selfModsList);
|
||||
|
||||
SelectTab(Tab.InstalledMods);
|
||||
}
|
||||
|
||||
private void SwitchContent(GUIFrame newContent)
|
||||
{
|
||||
contentFrame.Children.ForEach(c => c.Visible = false);
|
||||
newContent.Visible = true;
|
||||
}
|
||||
|
||||
public void SelectTab(Tab tab)
|
||||
{
|
||||
SwitchContent(tabContents[tab].Content);
|
||||
tabber.Children.ForEach(c =>
|
||||
{
|
||||
if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; }
|
||||
});
|
||||
if (!taskCancelSrc.IsCancellationRequested) { taskCancelSrc.Cancel(); }
|
||||
itemThumbnails.ForEach(t => t.Dispose());
|
||||
itemThumbnails.Clear();
|
||||
switch (tab)
|
||||
{
|
||||
case Tab.InstalledMods:
|
||||
PopulateInstalledModLists();
|
||||
break;
|
||||
case Tab.PopularMods:
|
||||
PopulateItemList(popularModsList, SteamManager.Workshop.GetPopularItems(), includeSubscribeButton: true);
|
||||
break;
|
||||
case Tab.Publish:
|
||||
PopulateItemList(selfModsList, SteamManager.Workshop.GetPublishedItems(), includeSubscribeButton: false, onFill: AddUnpublishedMods);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddButtonToTabber(Tab tab, GUIFrame content)
|
||||
{
|
||||
var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get($"workshopmenutab.{tab}"), style: "GUITabButton")
|
||||
{
|
||||
OnClicked = (b, _) =>
|
||||
{
|
||||
SelectTab(tab);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
button.RectTransform.MaxSize = RectTransform.MaxPoint;
|
||||
button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint);
|
||||
|
||||
tabContents.Add(tab, (button, content));
|
||||
}
|
||||
|
||||
private GUIFrame CreateNewContentFrame(Tab tab)
|
||||
{
|
||||
var content = new GUIFrame(new RectTransform(Vector2.One * 0.98f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null);
|
||||
AddButtonToTabber(tab, content);
|
||||
return content;
|
||||
}
|
||||
|
||||
private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars(
|
||||
GUIComponent parent,
|
||||
float leftWidth = 0.3875f,
|
||||
float centerWidth = 0.025f,
|
||||
float rightWidth = 0.5875f,
|
||||
bool split = false,
|
||||
float height = 1.0f)
|
||||
{
|
||||
GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true);
|
||||
GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false);
|
||||
var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null);
|
||||
if (split)
|
||||
{
|
||||
new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform),
|
||||
onDraw: (sb, c) =>
|
||||
{
|
||||
sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f);
|
||||
});
|
||||
}
|
||||
GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false);
|
||||
return (left, center, right);
|
||||
}
|
||||
|
||||
private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to)
|
||||
{
|
||||
if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null)
|
||||
{
|
||||
//move the dragged elements to the index determined previously
|
||||
var draggedElement = from.DraggedElement;
|
||||
|
||||
var selected = from.AllSelected.ToList();
|
||||
selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b));
|
||||
|
||||
float oldCount = to.Content.CountChildren;
|
||||
float newCount = oldCount + selected.Count;
|
||||
|
||||
var offset = draggedElement.RectTransform.AbsoluteOffset;
|
||||
offset += from.Content.Rect.Location;
|
||||
offset -= to.Content.Rect.Location;
|
||||
|
||||
for (int i = 0; i < selected.Count; i++)
|
||||
{
|
||||
var c = selected[i];
|
||||
c.Parent.RemoveChild(c);
|
||||
c.RectTransform.Parent = to.Content.RectTransform;
|
||||
c.RectTransform.RepositionChildInHierarchy((int)oldCount+i);
|
||||
}
|
||||
|
||||
from.DraggedElement = null;
|
||||
from.Deselect();
|
||||
from.RecalculateChildren();
|
||||
from.RectTransform.RecalculateScale(true);
|
||||
to.RecalculateChildren();
|
||||
to.RectTransform.RecalculateScale(true);
|
||||
to.Select(selected);
|
||||
|
||||
//recalculate the dragged element's offset so it doesn't jump around
|
||||
draggedElement.RectTransform.AbsoluteOffset = offset;
|
||||
|
||||
to.DraggedElement = draggedElement;
|
||||
|
||||
to.BarScroll = to.BarScroll * (oldCount / newCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateInstalledModsTab(
|
||||
out GUIDropDown enabledCoreDropdown,
|
||||
out GUIListBox enabledRegularModsList,
|
||||
out GUIListBox disabledRegularModsList,
|
||||
out Action<ItemOrPackage> onInstalledInfoButtonHit)
|
||||
{
|
||||
GUIFrame content = CreateNewContentFrame(Tab.InstalledMods);
|
||||
|
||||
CreateWorkshopItemDetailContainer(
|
||||
content,
|
||||
out var outerContainer,
|
||||
onSelected: (itemOrPackage, selectedFrame) =>
|
||||
{
|
||||
if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); }
|
||||
},
|
||||
onDeselected: PopulateInstalledModLists,
|
||||
out onInstalledInfoButtonHit, out var deselect);
|
||||
|
||||
GUILayoutGroup mainLayout =
|
||||
new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter);
|
||||
mainLayout.RectTransform.SetAsFirstChild();
|
||||
GUILayoutGroup coreSelectionLayout =
|
||||
new GUILayoutGroup(new RectTransform((0.5f, 0.15f), mainLayout.RectTransform));
|
||||
Label(coreSelectionLayout, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f / 0.15f);
|
||||
enabledCoreDropdown = Dropdown<CorePackage>(coreSelectionLayout,
|
||||
(p) => p.Name,
|
||||
ContentPackageManager.CorePackages.ToArray(),
|
||||
ContentPackageManager.EnabledPackages.Core!,
|
||||
(p) => { },
|
||||
heightScale: 1.0f / 0.15f);
|
||||
|
||||
var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.78f);
|
||||
right.ChildAnchor = Anchor.TopRight;
|
||||
|
||||
Action swapFunc(GUIListBox from, GUIListBox to)
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
to.Deselect();
|
||||
var selected = from.AllSelected.ToArray();
|
||||
foreach (var frame in selected)
|
||||
{
|
||||
frame.Parent.RemoveChild(frame);
|
||||
frame.RectTransform.Parent = to.Content.RectTransform;
|
||||
}
|
||||
from.RecalculateChildren();
|
||||
from.RectTransform.RecalculateScale(true);
|
||||
to.RecalculateChildren();
|
||||
to.RectTransform.RecalculateScale(true);
|
||||
to.Select(selected);
|
||||
};
|
||||
}
|
||||
|
||||
Action? currentCenterCallback = null;
|
||||
|
||||
//enabled mods
|
||||
Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont);
|
||||
var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.92f), left.RectTransform))
|
||||
{
|
||||
CurrentDragMode = GUIListBox.DragMode.DragOutsideBox,
|
||||
CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple,
|
||||
HideDraggedElement = true
|
||||
};
|
||||
enabledRegularModsList = enabledModsList;
|
||||
|
||||
//disabled mods
|
||||
Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont);
|
||||
var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.92f), right.RectTransform))
|
||||
{
|
||||
CurrentDragMode = GUIListBox.DragMode.DragOutsideBox,
|
||||
CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple,
|
||||
HideDraggedElement = true
|
||||
};
|
||||
disabledRegularModsList = disabledModsList;
|
||||
|
||||
var centerButton =
|
||||
new GUIButton(
|
||||
new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth,
|
||||
anchor: Anchor.Center),
|
||||
style: "GUIButtonToggleLeft")
|
||||
{
|
||||
Visible = false,
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
currentCenterCallback?.Invoke();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
enabledModsList.OnSelected = (frame, o) =>
|
||||
{
|
||||
disabledModsList.Deselect();
|
||||
|
||||
centerButton.Visible = true;
|
||||
centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight"));
|
||||
|
||||
currentCenterCallback = swapFunc(enabledModsList, disabledModsList);
|
||||
|
||||
return true;
|
||||
};
|
||||
disabledModsList.OnSelected = (frame, o) =>
|
||||
{
|
||||
enabledModsList.Deselect();
|
||||
|
||||
centerButton.Visible = true;
|
||||
centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft"));
|
||||
|
||||
currentCenterCallback = swapFunc(disabledModsList, enabledModsList);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f);
|
||||
searchRectT.RelativeSize = (0.5f, searchRectT.RelativeSize.Y);
|
||||
var searchHolder = new GUIFrame(searchRectT, style: null);
|
||||
var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "");
|
||||
var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft},
|
||||
textColor: Color.DarkGray * 0.6f,
|
||||
text: TextManager.Get("Search") + "...",
|
||||
textAlignment: Alignment.CenterLeft)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; };
|
||||
searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); };
|
||||
|
||||
searchBox.OnTextChanged += (sender, str) =>
|
||||
{
|
||||
enabledModsList.Content.Children.Concat(disabledModsList.Content.Children)
|
||||
.ForEach(c => c.Visible = str.IsNullOrWhiteSpace()
|
||||
|| (c.UserData is ContentPackage p
|
||||
&& p.Name.Contains(str, StringComparison.OrdinalIgnoreCase)));
|
||||
return true;
|
||||
};
|
||||
|
||||
new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform),
|
||||
onUpdate: (f, component) =>
|
||||
{
|
||||
HandleDraggingAcrossModLists(enabledModsList, disabledModsList);
|
||||
HandleDraggingAcrossModLists(disabledModsList, enabledModsList);
|
||||
},
|
||||
onDraw: (spriteBatch, component) =>
|
||||
{
|
||||
enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true);
|
||||
disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
private void PopulateInstalledModLists()
|
||||
{
|
||||
ContentPackageManager.UpdateContentPackageList();
|
||||
|
||||
SwapDropdownValues<CorePackage>(enabledCoreDropdown,
|
||||
(p) => p.Name,
|
||||
ContentPackageManager.CorePackages.ToArray(),
|
||||
ContentPackageManager.EnabledPackages.Core!,
|
||||
(p) => { });
|
||||
|
||||
void addRegularModToList(RegularPackage mod, GUIListBox list)
|
||||
{
|
||||
var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform),
|
||||
style: "ListBoxElement")
|
||||
{
|
||||
UserData = mod
|
||||
};
|
||||
|
||||
var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
|
||||
{
|
||||
Stretch = true,
|
||||
RelativeSpacing = 0.02f
|
||||
};
|
||||
|
||||
var dragIndicator = new GUIButton(new RectTransform((0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight),
|
||||
style: "GUIDragIndicator")
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
|
||||
var modNameScissor
|
||||
= new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform));
|
||||
var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), text: mod.Name);
|
||||
if (ContentPackageManager.LocalPackages.Contains(mod))
|
||||
{
|
||||
var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
|
||||
style: "WorkshopMenu.EditButton")
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
ToolBox.OpenFileWithShell(mod.Dir);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (ContentPackageManager.WorkshopPackages.Contains(mod))
|
||||
{
|
||||
var infoButton = new GUIButton(
|
||||
new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
|
||||
style: "WorkshopMenu.InfoButton")
|
||||
{
|
||||
OnClicked = (button, o) =>
|
||||
{
|
||||
TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId),
|
||||
t =>
|
||||
{
|
||||
if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; }
|
||||
if (item is null) { return; }
|
||||
onInstalledInfoButtonHit(item.Value);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
TaskPool.Add(
|
||||
$"DetermineUpdateRequired{mod.SteamWorkshopId}",
|
||||
mod.IsUpToDate(),
|
||||
t =>
|
||||
{
|
||||
if (!t.TryGetResult(out bool isUpToDate)) { return; }
|
||||
|
||||
if (!isUpToDate)
|
||||
{
|
||||
infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enabledRegularModsList.ClearChildren();
|
||||
for (int i = 0; i < ContentPackageManager.EnabledPackages.Regular.Count; i++)
|
||||
{
|
||||
var mod = ContentPackageManager.EnabledPackages.Regular[i];
|
||||
addRegularModToList(mod, enabledRegularModsList);
|
||||
}
|
||||
|
||||
disabledRegularModsList.ClearChildren();
|
||||
foreach (var mod in ContentPackageManager.RegularPackages)
|
||||
{
|
||||
if (ContentPackageManager.EnabledPackages.Regular.Contains(mod)) { continue; }
|
||||
addRegularModToList(mod, disabledRegularModsList);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePopularModsTab(out GUIListBox popularModsList)
|
||||
{
|
||||
GUIFrame content = CreateNewContentFrame(Tab.PopularMods);
|
||||
|
||||
CreateWorkshopItemList(content, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo);
|
||||
}
|
||||
|
||||
private void CreatePublishTab(out GUIListBox selfModsList)
|
||||
{
|
||||
GUIFrame content = CreateNewContentFrame(Tab.Publish);
|
||||
|
||||
CreateWorkshopItemOrPackageList(content, out _, out selfModsList, onSelected: PopulatePublishTab);
|
||||
}
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
ContentPackageManager.EnabledPackages.SetCore(enabledCorePackage);
|
||||
ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children
|
||||
.Where(c => c.UserData is RegularPackage).Select(c => (RegularPackage)c.UserData).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user