Client LuaCsForBarotrauma
ItemList.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Threading;
6 using System.Threading.Tasks;
8 using Barotrauma.IO;
9 using Microsoft.Xna.Framework;
10 using Microsoft.Xna.Framework.Graphics;
11 using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
12 
13 namespace Barotrauma.Steam
14 {
15  sealed partial class MutableWorkshopMenu : WorkshopMenu
16  {
17  private string ExtractTitle(ItemOrPackage itemOrPackage)
18  => itemOrPackage.TryGet(out ContentPackage package)
19  ? package.Name
20  : (((Steamworks.Ugc.Item)itemOrPackage).Title ?? "");
21 
22  private void CreateWorkshopItemDetailContainer(
23  GUIFrame parent,
24  out GUIListBox outerContainer,
25  Action<ItemOrPackage, GUIFrame> onSelected,
26  Action onDeselected,
27  out Action<ItemOrPackage> select,
28  out Action deselect)
29  {
30  ItemOrPackage? selectedItemOrPackage = null;
31 
32  GUIListBox outContainer = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform),
33  isHorizontal: true,
34  style: null)
35  {
36  ScrollBarEnabled = false,
37  ScrollBarVisible = false,
38  HoverCursor = CursorState.Default
39  };
40  outerContainer = outContainer;
41 
42  var selectedLayout =
43  new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform));
44  var selectedHeaderLayout =
45  new GUILayoutGroup(new RectTransform((1.0f, 0.05f), selectedLayout.RectTransform),
46  isHorizontal: true,
47  childAnchor: Anchor.CenterLeft);
48 
49  void deselectMethod()
50  {
51  if (selectedItemOrPackage is null) { return; }
52  selectedItemOrPackage = null;
53  onDeselected();
54  }
55 
56  deselect = deselectMethod;
57 
58  var backButton =
59  new GUIButton(new RectTransform((0.04f, 1.0f), selectedHeaderLayout.RectTransform),
60  style: "GUIButtonToggleLeft")
61  {
62  OnClicked = (button, o) =>
63  {
64  deselectMethod();
65  return false;
66  }
67  };
68  var padding = new GUIFrame(new RectTransform((1.0f, 0.005f), selectedLayout.RectTransform), style: null);
69  var selectedFrame = new GUIFrame(new RectTransform((1.0f, 0.945f), selectedLayout.RectTransform),
70  style: null);
71 
72  var selectionScroller = new GUICustomComponent(
73  new RectTransform(Vector2.Zero, outerContainer.Parent.RectTransform),
74  onUpdate: (deltaTime, component) =>
75  {
76  float targetScroll = selectedItemOrPackage is null
77  ? 0.0f
78  : 1.0f;
79  outContainer.ScrollBar.BarScroll
80  = MathUtils.NearlyEqual(targetScroll, outContainer.ScrollBar.BarScroll)
81  ? targetScroll
82  : MathHelper.Lerp(outContainer.ScrollBar.BarScroll, targetScroll, 0.3f);
83  });
84 
85  select = itemOrPackage =>
86  {
87  //showInSteamButton.Visible = itemOrPackage.TryGet(out Steamworks.Ugc.Item _);
88  //selectedItem = itemOrPackage;
89  //selectedTitle.Text = ExtractTitle(itemOrPackage);
90  selectedFrame.ClearChildren();
91 
92  //Jank to fix mouserect not clamping properly
93  //when shifting all elements to the left
94  var dropdowns = outContainer.Content.GetAllChildren<GUIDropDown>().ToArray();
95  var allChildren = outContainer.Content.GetAllChildren()
96  .Concat(selectedFrame.GetAllChildren());
97  allChildren.ForEach(c =>
98  {
99  //c.CascadingMouseRectClamp = !dropdowns.Any(dd => dd.IsParentOf(c) || dd.ListBox.IsParentOf(c));
100  //c.CanBeFocused = c.CanBeFocused || !c.CascadingMouseRectClamp;
101  c.ClampMouseRectToParent = !(c.Parent?.Parent is GUIDropDown);
102  }
103  );
104 
105  selectedItemOrPackage = itemOrPackage;
106  onSelected(itemOrPackage, selectedFrame);
107  };
108  }
109 
110  private void CreateWorkshopItemList(
111  GUIFrame parent,
112  out GUIListBox outerContainer,
113  out GUIListBox workshopItemList,
114  Action<Steamworks.Ugc.Item, GUIFrame> onSelected)
115  => CreateWorkshopItemOrPackageList(
116  parent,
117  out outerContainer,
118  out workshopItemList,
119  onSelected: (ItemOrPackage itemOrPackage, GUIFrame frame)
120  => onSelected((Steamworks.Ugc.Item)itemOrPackage, frame));
121 
122  private GUIButton CreateShowInSteamButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT)
123  => new GUIButton(
124  rectT,
125  TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall")
126  {
127  OnClicked = (button, o) =>
128  {
129  SteamManager.OverlayCustomUrl(workshopItem.Url);
130  return false;
131  }
132  };
133 
134  private GUIButton? CreateShowInSteamButton(ItemOrPackage itemOrPackage)
135  => itemOrPackage.TryGet(out Steamworks.Ugc.Item workshopItem)
136  ? CreateShowInSteamButton(workshopItem)
137  : null;
138 
139  private void CreateWorkshopItemOrPackageList(
140  GUIFrame parent,
141  out GUIListBox outerContainer,
142  out GUIListBox workshopItemList,
143  Action<ItemOrPackage, GUIFrame> onSelected)
144  {
145  GUIListBox? itemList = null;
146 
147  CreateWorkshopItemDetailContainer(
148  parent,
149  out outerContainer,
150  onSelected: onSelected,
151  onDeselected: () => itemList?.Deselect(),
152  out var select, out var deselect);
153 
154  itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform))
155  {
156  PlaySoundOnSelect = true,
157  };
158  itemList.RectTransform.SetAsFirstChild();
159  workshopItemList = itemList;
160 
161  var deselectCarrier
162  = CreateActionCarrier(outerContainer.Content, nameof(deselect).ToIdentifier(), deselect);
163 
164  itemList.OnSelected = (component, userData) =>
165  {
166  //Don't select if hitting the subscribe button
167  if (GUI.MouseOn.Parent != itemList.Content) { return false; }
168 
169  if (!(userData is ItemOrPackage itemOrPackage)) { return false; }
170 
171  select(itemOrPackage);
172 
173  return true;
174  };
175  }
176 
177  private void AddUnpublishedMods(ISet<Steamworks.Ugc.Item> workshopItems)
178  {
179  if (!selfModsListOption.TryUnwrap(out var selfModsList)) { return; }
180 
181  //Users that don't have a proper license cannot publish Workshop items
182  //(see https://partner.steamgames.com/doc/features/workshop#15)
183  void clearWithMessage(LocalizedString message)
184  {
185  selfModsList.ClearChildren();
186  var messageFrame = new GUIFrame(new RectTransform(Vector2.One, selfModsList.Content.RectTransform),
187  style: null)
188  {
189  CanBeFocused = false
190  };
191  new GUITextBlock(new RectTransform((0.5f, 1.0f), messageFrame.RectTransform, Anchor.Center),
192  text: message,
193  textAlignment: Alignment.Center,
194  wrap: true,
195  font: GUIStyle.Font);
196  }
197 
198  if (SteamManager.IsFreeWeekend())
199  {
200  clearWithMessage(TextManager.Get("FreeWeekendCantPublish"));
201  return;
202  }
203  if (SteamManager.IsFamilyShared())
204  {
205  clearWithMessage(TextManager.Get("FamilySharedCantPublish"));
206  return;
207  }
208 
209  DateTime getEditTime(ContentPackage p)
210  {
211  DateTime writeTime = File.GetLastWriteTime(p.Dir);
212 
213  //File.GetLastWriteTime on the directory is not good enough;
214  //it's possible to update a file in a directory without
215  //updating its parent directories' write time, so let's
216  //look at all of those files
217  var files = Directory.GetFiles(p.Dir, "*", System.IO.SearchOption.AllDirectories);
218  foreach (var file in files)
219  {
220  DateTime newTime = File.GetLastWriteTime(file);
221  if (newTime > writeTime) { writeTime = newTime; }
222  }
223 
224  return writeTime;
225  }
226 
227  //Find local packages associated with the Workshop items if available
228  (Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems
229  .Select(item => (item,
230  (ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p
231  => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == item.Id)))
232  //Sort the pairs by last local edit time if available
233  .OrderBy(t => t.Item2 == null)
234  .ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime)
235  .ToArray();
236 
237  int indexOfUserDataInPublishedItemsArray(object userData)
238  => publishedItems.IndexOf(t
239  => t.WorkshopItem.Id == ((Steamworks.Ugc.Item)(userData as ItemOrPackage)!).Id);
240 
241  //Take the existing GUI items that are in the list and sort to match the order of publishedItems
242  var publishedGuiComponents = selfModsList.Content.Children.OrderBy(c => indexOfUserDataInPublishedItemsArray(c.UserData)).ToArray();
243 
244  //Get mods that haven't been published and add them to the list
245  var unpublishedMods = ContentPackageManager.LocalPackages
246  .Where(p =>
247  !p.TryExtractSteamWorkshopId(out var workshopId)
248  || !publishedItems.Any(item => item.WorkshopItem.Id == workshopId.Value))
249  .OrderByDescending(getEditTime).ToArray();
250 
251  if (unpublishedMods.Any())
252  {
253  var unpublishedHeader
254  = new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform),
255  TextManager.Get("UnpublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
256  }
257 
258  foreach (var unpublishedMod in unpublishedMods)
259  {
260  var unpublishedFrame = new GUIFrame(
261  new RectTransform((1.0f, 1.0f / 5.5f), selfModsList.Content.RectTransform),
262  style: "ListBoxElement")
263  {
264  UserData = (ItemOrPackage)unpublishedMod
265  };
266  var unpublishedLayout
267  = new GUILayoutGroup(new RectTransform(Vector2.One, unpublishedFrame.RectTransform),
268  isHorizontal: true)
269  {
270  Stretch = true,
271  RelativeSpacing = 0.02f
272  };
273  var unpublishedPadding
274  = new GUIFrame(
275  new RectTransform(Vector2.One, unpublishedLayout.RectTransform,
276  scaleBasis: ScaleBasis.BothHeight), style: null)
277  {
278  CanBeFocused = false
279  };
280  var unpublishedTextBlock
281  = new GUITextBlock(new RectTransform(Vector2.One, unpublishedLayout.RectTransform),
282  $"{unpublishedMod.Name}\n\n" +
283  TextManager.GetWithVariable("LastLocalEditTime",
284  "[datetime]",
285  getEditTime(unpublishedMod).ToString()),
286  font: GUIStyle.Font)
287  {
288  CanBeFocused = false
289  };
290  unpublishedLayout.Recalculate();
291  }
292 
293  if (publishedGuiComponents.Any())
294  {
295  var publishedHeader
296  = new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform),
297  TextManager.Get("PublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
298  }
299 
300  foreach (var c in publishedGuiComponents)
301  {
302  c.SetAsLastChild();
303  var textBlock = (c.FindChild(b => b is GUITextBlock, recursive: true) as GUITextBlock)!;
304  textBlock.Text += $"\n";
305 
306  int index = indexOfUserDataInPublishedItemsArray(c.UserData);
307  (Steamworks.Ugc.Item workshopItem, ContentPackage? localMod) = publishedItems[index];
308  if (localMod != null)
309  {
310  textBlock.Text += $"\n" + TextManager.GetWithVariable("LastLocalEditTime", "[datetime]", getEditTime(localMod).ToString());
311  }
312  textBlock.Text += $"\n" + TextManager.GetWithVariable("LatestPublishTime", "[datetime]", workshopItem.LatestUpdateTime.ToLocalTime().ToString());
313  }
314  }
315 
316  private static (GUIButton Button, GUIFrame Sprite) CreatePaddedButton(RectTransform rectT, string style, float spriteScale)
317  {
318  var button = new GUIButton(
319  rectT,
320  style: null);
321 
322  var sprite = new GUIFrame(
323  new RectTransform(Vector2.One * spriteScale, button.RectTransform, Anchor.Center),
324  style: style)
325  {
326  CanBeFocused = false
327  };
328 
329  return (button, sprite);
330  }
331 
332  private static void CreateSubscribeButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT, float spriteScale)
333  {
334  const string plusButton = "GUIPlusButton";
335  const string minusButton = "GUIMinusButton";
336 
337  LocalizedString subscribeTooltip = TextManager.Get("DownloadButton");
338  LocalizedString unsubscribeTooptip = TextManager.Get("WorkshopItemUnsubscribe");
339 
340  var (subscribeButton, subscribeButtonSprite) = CreatePaddedButton(rectT, plusButton, spriteScale);
341  subscribeButton.ToolTip = subscribeTooltip;
342 
343  subscribeButton.OnClicked = (button, o) =>
344  {
345  if (!SteamManager.IsInitialized) { return false; }
346 
347  if (!workshopItem.IsSubscribed)
348  {
349  workshopItem.Subscribe();
350  TaskPool.Add($"DownloadSubscribedItem{workshopItem.Id}",
351  SteamManager.Workshop.ForceRedownload(workshopItem),
352  TaskPool.IgnoredCallback);
353  }
354  else
355  {
356  workshopItem.Unsubscribe();
357  SteamManager.Workshop.Uninstall(workshopItem);
358  }
359 
360  return false;
361  };
362 
363  var buttonStyleUpdater = new GUICustomComponent(
364  new RectTransform(Vector2.Zero, subscribeButton.RectTransform),
365  onUpdate: (deltaTime, component) =>
366  {
367  if (!SteamManager.IsInitialized) { return; }
368 
369  if (subscribeButtonSprite.Style is { Identifier: { } styleId })
370  {
371  if (workshopItem.IsSubscribed && styleId != minusButton)
372  {
373  subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(minusButton));
374  subscribeButton.ToolTip = unsubscribeTooptip;
375  }
376  if (!workshopItem.IsSubscribed && styleId != plusButton)
377  {
378  subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(plusButton));
379  subscribeButton.ToolTip = subscribeTooltip;
380  }
381  }
382  });
383 
384  float displayedDownloadAmount = workshopItem.DownloadAmount;
385  var downloadProgressBar = new GUICustomComponent(
386  new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center),
387  onDraw: (spriteBatch, component) =>
388  {
389  if (!SteamManager.IsInitialized) { return; }
390 
391  bool visible = workshopItem.IsSubscribed
392  && (workshopItem.IsDownloading
393  || workshopItem.IsDownloadPending
394  || !MathUtils.NearlyEqual(workshopItem.DownloadAmount, displayedDownloadAmount));
395  if (!visible) { return; }
396 
397  void drawSection(float amount, Color color, float thickness)
398  => GUI.DrawDonutSection(
399  spriteBatch,
400  component.Rect.Center.ToVector2() + (0, 1),
401  new Range<float>(component.Rect.Width * 0.55f - thickness * 0.5f, component.Rect.Width * 0.55f + thickness * 0.5f),
402  amount * MathF.PI * 2.0f,
403  color);
404 
405  void drawSectionFuzzy(float amount, Color color, float thickness)
406  {
407  drawSection(amount, color, thickness);
408  drawSection(amount, color * 0.6f, thickness + 0.5f);
409  drawSection(amount, color * 0.3f, thickness + 1.0f);
410  }
411 
412  drawSectionFuzzy(1.0f, Color.Lerp(Color.Black, GUIStyle.Blue, 0.2f), component.Rect.Width * 0.25f);
413  drawSectionFuzzy(1.0f, Color.Black, component.Rect.Width * 0.15f);
414  drawSectionFuzzy(displayedDownloadAmount, GUIStyle.Green, component.Rect.Width * 0.08f);
415  },
416  onUpdate: (deltaTime, component) =>
417  {
418  if (!SteamManager.IsInitialized) { return; }
419 
420  displayedDownloadAmount = Math.Min(
421  workshopItem.DownloadAmount,
422  MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f));
423  })
424  {
425  CanBeFocused = false
426  };
427  }
428 
429  private void PopulateItemList(GUIListBox itemListBox, Task<ISet<Steamworks.Ugc.Item>> items, bool includeSubscribeButton, Action<ISet<Steamworks.Ugc.Item>>? onFill = null)
430  {
431  itemListBox.ClearChildren();
432  itemListBox.Deselect();
433  itemListBox.ScrollBar.BarScroll = 0.0f;
434  TaskPool.AddIfNotFound("PopulateTabWithItemList", items,
435  (t) =>
436  {
437  taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc;
438  itemListBox.ClearChildren();
439  var workshopItems = ((Task<ISet<Steamworks.Ugc.Item>>)t).Result;
440  foreach (var workshopItem in workshopItems)
441  {
442  var itemFrame = new GUIFrame(
443  new RectTransform((1.0f, 1.0f / 5.5f), itemListBox.Content.RectTransform),
444  style: "ListBoxElement")
445  {
446  UserData = (ItemOrPackage)workshopItem
447  };
448  var itemLayout = new GUILayoutGroup(
449  new RectTransform(Vector2.One, itemFrame.RectTransform),
450  isHorizontal: true, childAnchor: Anchor.CenterLeft)
451  {
452  Stretch = true
453  };
454 
455  var thumbnailContainer
456  = CreateThumbnailContainer(itemLayout, Vector2.One, ScaleBasis.BothHeight);
457  CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer);
458  thumbnailContainer.CanBeFocused = false;
459  thumbnailContainer.GetAllChildren().ForEach(c => c.CanBeFocused = false);
460 
461  var title = new GUITextBlock(
462  new RectTransform(Vector2.One, itemLayout.RectTransform),
463  workshopItem.Title ?? "", font: GUIStyle.Font)
464  {
465  CanBeFocused = false
466  };
467 
468  if (includeSubscribeButton)
469  {
470  CreateSubscribeButton(workshopItem, new RectTransform(Vector2.One, itemLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), spriteScale: 0.4f);
471  }
472  itemLayout.Recalculate();
473  }
474  onFill?.Invoke(workshopItems);
475  });
476  }
477 
478  private GUIFrame CreateThumbnailContainer(
479  GUIComponent parent,
480  Vector2 relativeSize,
481  ScaleBasis scaleBasis)
482  => new GUIFrame(new RectTransform(relativeSize, parent.RectTransform, scaleBasis: scaleBasis),
483  style: "GUIFrameListBox");
484 
485  private SteamManager.Workshop.ItemThumbnail CreateItemThumbnail(
486  in Steamworks.Ugc.Item workshopItem,
487  CancellationToken cancellationToken,
488  GUIFrame thumbnailContainer)
489  {
490  var thumbnail = new SteamManager.Workshop.ItemThumbnail(workshopItem, cancellationToken);
491  itemThumbnails.Add(thumbnail);
492  CreateAsyncThumbnailComponent(thumbnailContainer, () => thumbnail.Texture, () => thumbnail.Loading);
493  return thumbnail;
494  }
495 
496  private GUICustomComponent CreateAsyncThumbnailComponent(GUIFrame thumbnailContainer, Func<Texture2D?> textureGetter, Func<bool> throbberEnabled)
497  {
498  int randomThrobberOffset = Rand.Range(0, 10, Rand.RandSync.Unsynced);
499  return new GUICustomComponent(
500  new RectTransform(Vector2.One, thumbnailContainer.RectTransform, Anchor.Center),
501  onDraw: (spriteBatch, component) =>
502  {
503  Rectangle rect = component.Rect;
504  Texture2D? texture = textureGetter();
505  if (texture != null)
506  {
507  rect.Location += (4, 4);
508  rect.Size -= (8, 8);
509  Point destinationSizeMaxWidth = (rect.Width, rect.Width * texture.Height / texture.Width);
510  Point destinationSizeMaxHeight = (rect.Height * texture.Width / texture.Height, rect.Height);
511  Point destinationSize = destinationSizeMaxHeight.X > rect.Width
512  ? destinationSizeMaxWidth
513  : destinationSizeMaxHeight;
514  Rectangle destinationRectangle = new Rectangle(
515  rect.Center.X - destinationSize.X / 2,
516  rect.Center.Y - destinationSize.Y / 2,
517  destinationSize.X,
518  destinationSize.Y);
519  spriteBatch.Draw(texture, destinationRectangle, Color.White);
520  }
521  else if (throbberEnabled())
522  {
523  var sheet = GUIStyle.GenericThrobber;
524  Vector2 pos = rect.Center.ToVector2() - Vector2.One * rect.Height * 0.4f;
525  sheet.Draw(spriteBatch, ((int)Math.Floor(Timing.TotalTime * 24.0f) + randomThrobberOffset) % sheet.FrameCount, pos, Color.White,
526  origin: Vector2.Zero, rotate: 0.0f,
527  scale: Vector2.One * component.Rect.Height / sheet.FrameSize.ToVector2() * 0.8f);
528  }
529  });
530  }
531 
532  private GUIListBox CreateTagsList(IEnumerable<Identifier> tags, RectTransform rectT, bool canBeFocused)
533  {
534  var tagsList
535  = new GUIListBox(rectT, style: null, isHorizontal: false)
536  {
537  UseGridLayout = true,
538  ScrollBarEnabled = false,
539  ScrollBarVisible = false,
540  HideChildrenOutsideFrame = false,
541  Spacing = GUI.IntScale(4)
542  };
543  tagsList.Content.ClampMouseRectToParent = false;
544  foreach (Identifier tag in tags)
545  {
546  var tagBtn = new GUIButton(
547  new RectTransform(new Vector2(0.25f, 1.0f / 8.0f), tagsList.Content.RectTransform,
548  anchor: Anchor.TopLeft),
549  TextManager.Get($"workshop.contenttag.{tag.Value.RemoveWhitespace()}")
550  .Fallback(tag.Value.CapitaliseFirstInvariant()), style: "GUIButtonRound")
551  {
552  CanBeFocused = canBeFocused,
553  Selected = !canBeFocused,
554  UserData = tag
555  };
556  tagBtn.RectTransform.NonScaledSize
557  = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(15), GUI.IntScale(5));
558  tagBtn.RectTransform.IsFixedSize = true;
559  tagBtn.ClampMouseRectToParent = false;
560  }
561 
562  return tagsList;
563  }
564 
565  private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame)
566  {
567  ViewingItemDetails = true;
568  taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc;
569 
570  var contentPackage
571  = ContentPackageManager.WorkshopPackages.FirstOrDefault(p =>
572  p.TryExtractSteamWorkshopId(out var workshopId)
573  && workshopId.Value == workshopItem.Id);
574 
575  var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform));
576 
577  var headerLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.1f), verticalLayout.RectTransform),
578  isHorizontal: true) { Stretch = true };
579 
580  var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform));
581 
582  var selectedTitle =
583  new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title ?? "",
584  font: GUIStyle.LargeFont);
585 
586  var author = workshopItem.Owner;
587  var authorButton = new GUIButton(new RectTransform((1.0f, 0.5f),
588  titleAndAuthorLayout.RectTransform),
589  style: null,
590  textAlignment: Alignment.CenterLeft)
591  {
593  Font = GUIStyle.SubHeadingFont,
594  TextColor = GUIStyle.TextColorNormal,
595  HoverTextColor = Color.White,
596  SelectedTextColor = GUIStyle.TextColorNormal,
597  OnClicked = (button, o) =>
598  {
599  SteamManager.OverlayCustomUrl(
600  $"https://steamcommunity.com/profiles/{author.Id}/myworkshopfiles/?appid={SteamManager.AppID}");
601  return false;
602  }
603  };
604  var authorPadding = authorButton.GetChild<GUITextBlock>().Padding;
605 
606  RectTransform rightSideButtonRectT()
607  => new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight);
608 
609  bool reinstallAction(GUIButton button, object o)
610  {
611  SettingsMenu.Instance?.ApplyInstalledModChanges();
612  int prevIndex = ContentPackageManager.EnabledPackages.Regular.IndexOf(contentPackage);
613  TaskPool.AddIfNotFound($"Reinstall{workshopItem.Id}",
614  SteamManager.Workshop.Reinstall(workshopItem), t =>
615  {
616  ContentPackageManager.WorkshopPackages.Refresh();
617  ContentPackageManager.EnabledPackages.RefreshUpdatedMods();
618  if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && !mutableWorkshopMenu.ViewingItemDetails)
619  {
620  mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true);
621  }
622  });
623  return false;
624  }
625 
626  var (updateButton, updateSprite) = CreatePaddedButton(
627  rightSideButtonRectT(),
628  "GUIUpdateButton",
629  spriteScale: 0.8f);
630  updateButton.ToolTip = TextManager.Get("WorkshopItemUpdate");
631  updateButton.Visible = false;
632  updateButton.OnClicked = reinstallAction;
633 
634  if (contentPackage != null)
635  {
636  TaskPool.AddIfNotFound(
637  $"DetermineUpdateRequired{contentPackage.UgcId}",
638  contentPackage.IsUpToDate(),
639  t =>
640  {
641  if (!t.TryGetResult(out bool isUpToDate)) { return; }
642 
643  updateButton.Visible = !isUpToDate;
644  });
645  }
646 
647  var (reinstallButton, reinstallSprite) = CreatePaddedButton(
648  rightSideButtonRectT(),
649  "GUIReloadButton",
650  spriteScale: 0.8f);
651  reinstallButton.ToolTip = TextManager.Get("WorkshopItemReinstall");
652  reinstallButton.OnClicked = reinstallAction;
653  var reinstallButtonUpdater = new GUICustomComponent(
654  new RectTransform(Vector2.Zero, reinstallButton.RectTransform),
655  onUpdate: (f, component) =>
656  {
657  reinstallButton.Visible = workshopItem.IsSubscribed
658  || workshopItem.Owner.Id == SteamManager.GetSteamId().Select(steamId => steamId.Value).Fallback(0);
659  reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending
660  && !SteamManager.Workshop.IsInstalling(workshopItem);
661 
662  reinstallSprite.Color = reinstallButton.Enabled
663  ? reinstallSprite.Style.Color
664  : Color.DimGray;
665  updateButton.Enabled = reinstallButton.Enabled && contentPackage != null && ContentPackageManager.WorkshopPackages.Contains(contentPackage);
666  updateSprite.Color = reinstallSprite.Color;
667 
668  if (contentPackage != null
669  && !ContentPackageManager.WorkshopPackages.Contains(contentPackage)
670  && ContentPackageManager.WorkshopPackages.Any(p =>
671  p.TryExtractSteamWorkshopId(out var workshopId)
672  && workshopId.Value == workshopItem.Id))
673  {
674  updateButton.Visible = false;
675  }
676  });
677  CreateSubscribeButton(workshopItem,
678  rightSideButtonRectT(),
679  spriteScale: 0.8f);
680 
681  var padding = new GUIFrame(
682  new RectTransform((0.15f, 1.0f), headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight),
683  style: null);
684 
685  padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null);
686 
687  var horizontalLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.45f), verticalLayout.RectTransform),
688  isHorizontal: true)
689  {
690  Stretch = true
691  };
692 
693  TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) =>
694  {
695  authorButton.Text = author.Name ?? "";
696  authorButton.RectTransform.NonScaledSize =
697  ((int)(authorButton.Font.MeasureString(author.Name ?? "").X + authorPadding.X + authorPadding.Z),
698  authorButton.RectTransform.NonScaledSize.Y);
699  });
700 
701  var thumbnailSuperContainer = new GUIFrame(
702  new RectTransform(Vector2.One, horizontalLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight),
703  style: null);
704  GUIFrame thumbnailContainer = CreateThumbnailContainer(thumbnailSuperContainer, Vector2.One,
705  scaleBasis: ScaleBasis.BothHeight);
706  CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer);
707  thumbnailContainer.RectTransform.Anchor = Anchor.Center;
708  thumbnailContainer.RectTransform.Pivot = Pivot.Center;
709 
710  var statsBox = new GUIFrame(new RectTransform((0.6f, 1.0f), horizontalLayout.RectTransform),
711  style: "GUIFrameListBox");
712 
713  #region Stats box
714  var statsHorizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, statsBox.RectTransform), isHorizontal: true);
715  var statsVertical0
716  = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform), childAnchor: Anchor.TopCenter);
717 
718  statFrame("", ""); //padding
719 
720  var scoreFrame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), style: null);
721  var scoreLabel = new GUITextBlock(new RectTransform((0.4f, 1.0f), scoreFrame.RectTransform),
722  TextManager.Get("WorkshopItemScore"), font: GUIStyle.SubHeadingFont);
723  var scoreStarContainer
724  = new GUILayoutGroup(
725  new RectTransform((0.6f, 1.0f), scoreFrame.RectTransform, Anchor.CenterRight),
726  isHorizontal: true,
727  childAnchor: Anchor.CenterLeft) { Stretch = true };
728  var starColor = Color.Lerp(
729  Color.Lerp(Color.White, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)),
730  Color.Lime, Math.Max(0.0f, (workshopItem.Score - 0.5f) * 2.0f));
731  for (int i = 0; i < 5; i++)
732  {
733  bool isStarLit = i <= Round(workshopItem.Score * 5.0f);
734  var star = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
735  style: isStarLit ? "GUIStarIconBright" : "GUIStarIconDark");
736  if (isStarLit)
737  {
738  star.Color = starColor;
739  star.HoverColor = starColor;
740  star.SelectedColor = starColor;
741  }
742  }
743  var scoreTextPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
744  style: null);
745 
746  var scoreTextContainer = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform),
747  style: null);
748 
749  var scoreVoteCount = new GUITextBlock(
750  new RectTransform((1.0f, 1.5f), scoreTextContainer.RectTransform, Anchor.Center),
751  TextManager.GetWithVariable("WorkshopItemVotes", "[VoteCount]",
752  (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.BottomLeft)
753  {
754  Padding = Vector4.Zero
755  };
756  var subscriptionCount = new GUITextBlock(
757  new RectTransform((1.0f, 1.5f), scoreTextContainer.RectTransform, Anchor.Center),
758  TextManager.GetWithVariable("WorkshopItemSubscriptions", "[SubscriptionCount]",
759  workshopItem.NumUniqueSubscriptions.ToString()), textAlignment: Alignment.TopLeft)
760  {
761  Padding = Vector4.Zero
762  };
763 
764  void statFrame(LocalizedString labelText, LocalizedString dataText)
765  {
766  var frame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0!.RectTransform), style: null);
767  var label = new GUITextBlock(new RectTransform((0.4f, 1.0f), frame.RectTransform),
768  labelText, font: GUIStyle.SubHeadingFont);
769  var data = new GUITextBlock(new RectTransform((0.6f, 1.0f), frame.RectTransform, Anchor.CenterRight),
770  dataText, font: GUIStyle.Font)
771  {
772  Padding = Vector4.Zero
773  };
774  }
775 
776  statFrame(TextManager.Get("WorkshopItemFileSize"), MathUtils.GetBytesReadable(workshopItem.SizeOfFileInBytes));
777  statFrame(TextManager.Get("WorkshopItemCreationDate"), workshopItem.Created.ToShortDateString());
778  statFrame(TextManager.Get("WorkshopItemModificationDate"), workshopItem.Updated.ToShortDateString());
779 
780  var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform),
781  TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont);
782  CreateTagsList((workshopItem.Tags ?? Array.Empty<string>()).ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false);
783  #endregion
784 
785  var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform));
786  CreateBBCodeElement(workshopItem, descriptionListBox);
787 
788  var showInSteamContainer
789  = new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null);
790  CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), showInSteamContainer.RectTransform, Anchor.CenterRight));
791  }
792  }
793 }
GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action)
CursorState
Definition: GUI.cs:40