Barotrauma Client Doc
CrewManagement.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
8 
9 namespace Barotrauma
10 {
12  {
13  private CampaignMode campaign => campaignUI.Campaign;
14  private readonly CampaignUI campaignUI;
15  private readonly GUIComponent parentComponent;
16 
17  private GUILayoutGroup pendingAndCrewGroup;
18  private GUIListBox hireableList, pendingList, crewList;
19  private GUIFrame characterPreviewFrame;
20  private GUIDropDown sortingDropDown;
21  private GUITextBlock totalBlock;
22  private GUIButton validateHiresButton;
23  private GUIButton clearAllButton;
24 
25  private PlayerBalanceElement? playerBalanceElement;
26 
27  private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires;
28 
29  // Is the player hiring a new character for themselves instead of bots for the crew?
30  // The window can only be used for one of these purposes at the same time.
31  private static bool HiringNewCharacter => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } &&
32  GameMain.Client?.CharacterInfo is { PermanentlyDead: true };
33 
34  private static bool HasPermissionToHire => CampaignMode.AllowedToManageCampaign(
35  HiringNewCharacter ? ClientPermissions.ManageMoney : ClientPermissions.ManageHires);
36 
37  private Point resolutionWhenCreated;
38 
39  private bool needsHireableRefresh;
40 
41  private enum SortingMethod
42  {
43  AlphabeticalAsc,
44  JobAsc,
45  PriceAsc,
46  PriceDesc,
47  SkillAsc,
48  SkillDesc
49  }
50 
51  public CrewManagement(CampaignUI campaignUI, GUIComponent parentComponent)
52  {
53  this.campaignUI = campaignUI;
54  this.parentComponent = parentComponent;
55 
56  CreateUI();
57  UpdateLocationView(campaignUI.Campaign.Map.CurrentLocation, true);
58 
59  campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting(
60  "CrewManagement.UpdateLocationView".ToIdentifier(),
61  (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation));
62  Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting(
63  "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true);
64  }
65 
66  public void RefreshPermissions()
67  {
68  RefreshCrewFrames(hireableList);
69  RefreshCrewFrames(crewList);
70  RefreshCrewFrames(pendingList);
71  if (clearAllButton != null) { clearAllButton.Enabled = HasPermissionToHire; }
72  }
73 
74  private void RefreshCrewFrames(GUIListBox listBox)
75  {
76  if (listBox == null) { return; }
77  listBox.CanBeFocused = HasPermissionToHire;
78  foreach (GUIComponent child in listBox.Content.Children)
79  {
80  if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton)
81  {
82  CharacterInfo characterInfo = buyButton.UserData as CharacterInfo;
83  bool enougMoneyToHire = !HiringNewCharacter || campaign.CanAfford(HireManager.GetSalaryFor(characterInfo));
84  buyButton.Enabled = HasPermissionToHire && EnoughReputationToHire(characterInfo) && enougMoneyToHire;
85  foreach (GUITextBlock text in child.GetAllChildren<GUITextBlock>())
86  {
87  text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f);
88  }
89  }
90  }
91  }
92 
93  private void CreateUI()
94  {
95  if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild)
96  {
97  parentComponent.RemoveChild(glowChild);
98  }
99  if (parentComponent.FindChild(c => c.UserData as string == "container") is GUIComponent containerChild)
100  {
101  parentComponent.RemoveChild(containerChild);
102  }
103 
104  new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center),
105  style: "OuterGlow", color: Color.Black * 0.7f)
106  {
107  UserData = "glow"
108  };
109  new GUIFrame(new RectTransform(new Vector2(0.95f), parentComponent.RectTransform, anchor: Anchor.Center),
110  style: null)
111  {
112  CanBeFocused = false,
113  UserData = "container"
114  };
115 
116  int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560));
117  var availableMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform)
118  {
119  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
120  })
121  {
122  Stretch = true,
123  RelativeSpacing = 0.02f
124  };
125 
126  // Header ------------------------------------------------
127  var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), availableMainGroup.RectTransform), isHorizontal: true)
128  {
129  RelativeSpacing = 0.005f
130  };
131  var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width;
132  new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "CrewManagementHeaderIcon");
133  new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUIStyle.LargeFont)
134  {
135  CanBeFocused = false,
137  };
138 
139  var hireablesGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center,
140  parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), availableMainGroup.RectTransform)).RectTransform))
141  {
142  RelativeSpacing = 0.015f,
143  Stretch = true
144  };
145 
146  var sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), hireablesGroup.RectTransform), isHorizontal: true)
147  {
148  RelativeSpacing = 0.015f,
149  Stretch = true
150  };
151  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby"));
152  sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), elementCount: 5)
153  {
154  OnSelected = (child, userData) =>
155  {
156  SortCharacters(hireableList, (SortingMethod)userData);
157  return true;
158  }
159  };
160  var tag = "sortingmethod.";
161  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.JobAsc), userData: SortingMethod.JobAsc);
162  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillAsc), userData: SortingMethod.SkillAsc);
163  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillDesc), userData: SortingMethod.SkillDesc);
164  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceAsc), userData: SortingMethod.PriceAsc);
165  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceDesc), userData: SortingMethod.PriceDesc);
166 
167  hireableList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.96f),
168  hireablesGroup.RectTransform,
169  anchor: Anchor.Center))
170  {
171  Spacing = 1
172  };
173 
174  var pendingAndCrewMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform, anchor: Anchor.TopRight)
175  {
176  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
177  })
178  {
179  Stretch = true,
180  RelativeSpacing = 0.02f
181  };
182 
183  playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f));
184 
185  pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center,
186  parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform)
187  {
188  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
189  }).RectTransform));
190 
191  float height = 0.05f;
192  new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont);
193  pendingList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform))
194  {
195  Spacing = 1
196  };
197 
198  var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont);
199 
200  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), crewHeader.RectTransform, Anchor.CenterRight), string.Empty, textAlignment: Alignment.CenterRight)
201  {
202  TextGetter = () =>
203  {
204  int crewSize = campaign?.CrewManager?.GetCharacterInfos()?.Count() ?? 0;
205  return $"{crewSize}/{CrewManager.MaxCrewSize}";
206  }
207  };
208  crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform))
209  {
210  Spacing = 1
211  };
212 
213  var group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true);
214  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), TextManager.Get("campaignstore.total"));
215  totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right)
216  {
217  TextScale = 1.1f
218  };
219  group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight)
220  {
221  RelativeSpacing = 0.01f
222  };
223  validateHiresButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate"))
224  {
225  ClickSound = GUISoundType.ConfirmTransaction,
227  OnClicked = (b, o) => ValidateHires(PendingHires, createNetworkEvent: true)
228  };
229  clearAllButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall"))
230  {
231  ClickSound = GUISoundType.Cart,
233  Enabled = HasPermissionToHire,
234  OnClicked = (b, o) => RemoveAllPendingHires()
235  };
236  GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock);
237 
238  resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
239  }
240 
241  private void UpdateLocationView(Location location, bool removePending, Location prevLocation = null)
242  {
243  if (prevLocation != null && prevLocation == location && GameMain.NetworkMember != null) { return; }
244 
245  if (characterPreviewFrame != null)
246  {
247  characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
248  characterPreviewFrame = null;
249  }
250  UpdateHireables(location);
251  if (pendingList != null)
252  {
253  if (removePending)
254  {
255  PendingHires?.Clear();
256  pendingList.Content.ClearChildren();
257  }
258  else
259  {
260  PendingHires?.ForEach(ci => AddPendingHire(ci));
261  }
262  SetTotalHireCost();
263  }
264  UpdateCrew();
265  }
266 
267  public void UpdateHireables()
268  {
269  UpdateHireables(campaign?.CurrentLocation);
270  }
271 
272  private void UpdateHireables(Location location)
273  {
274  if (hireableList == null) { return; }
275  hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c));
276  var hireableCharacters = location.GetHireableCharacters();
277  if (hireableCharacters.None())
278  {
279  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center)
280  {
281  CanBeFocused = false
282  };
283  }
284  else
285  {
286  foreach (CharacterInfo c in hireableCharacters)
287  {
288  if (c == null || PendingHires.Contains(c)) { continue; }
289  CreateCharacterFrame(c, hireableList);
290  }
291  }
292  sortingDropDown.SelectItem(SortingMethod.JobAsc);
293  hireableList.UpdateScrollBarSize();
294  }
295 
296  public void SetHireables(Location location, List<CharacterInfo> availableHires)
297  {
298  HireManager hireManager = location.HireManager;
299  if (hireManager == null) { return; }
300  int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.ID);
301  int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.ID);
302  if (hireVal != newVal)
303  {
304  location.HireManager.AvailableCharacters = availableHires;
305  UpdateHireables(location);
306  }
307  }
308 
309  public void UpdateCrew()
310  {
311  crewList.Content.Children.ToList().ForEach(c => crewList.Content.RemoveChild(c));
313  {
314  if (c == null || !((c.Character?.IsBot ?? true) || campaign is SinglePlayerCampaign)) { continue; }
315  CreateCharacterFrame(c, crewList);
316  }
317  SortCharacters(crewList, SortingMethod.JobAsc);
318  crewList.UpdateScrollBarSize();
319  }
320 
321  private void SortCharacters(GUIListBox list, SortingMethod sortingMethod)
322  {
323  if (sortingMethod == SortingMethod.AlphabeticalAsc)
324  {
325  list.Content.RectTransform.SortChildren((x, y) =>
326  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
327  ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name));
328  }
329  else if (sortingMethod == SortingMethod.JobAsc)
330  {
331  SortCharacters(list, SortingMethod.AlphabeticalAsc);
332  list.Content.RectTransform.SortChildren((x, y) =>
333  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
334  string.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal));
335  }
336  else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc)
337  {
338  SortCharacters(list, SortingMethod.AlphabeticalAsc);
339  list.Content.RectTransform.SortChildren((x, y) =>
340  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
341  ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary));
342  if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); }
343  }
344  else if (sortingMethod == SortingMethod.SkillAsc || sortingMethod == SortingMethod.SkillDesc)
345  {
346  SortCharacters(list, SortingMethod.AlphabeticalAsc);
347  list.Content.RectTransform.SortChildren((x, y) =>
348  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
349  ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel));
350  if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); }
351  }
352 
353  int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2)
354  {
355  CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo;
356  CharacterInfo info2 = ((InfoSkill)c2.UserData).CharacterInfo;
357  float requirement1 = EnoughReputationToHire(info1) ? 0 : info1.MinReputationToHire.reputation;
358  float requirement2 = EnoughReputationToHire(info2) ? 0 : info2.MinReputationToHire.reputation;
359  if (MathUtils.NearlyEqual(requirement1, 0.0f) && MathUtils.NearlyEqual(requirement2, 0.0f))
360  {
361  return null;
362  }
363  else
364  {
365  return requirement1.CompareTo(requirement2);
366  }
367  }
368  }
369 
370  private readonly struct InfoSkill
371  {
372  public readonly CharacterInfo CharacterInfo;
373  public readonly float SkillLevel;
374 
375  public InfoSkill(CharacterInfo characterInfo, float skillLevel)
376  {
377  CharacterInfo = characterInfo;
378  SkillLevel = skillLevel;
379  }
380  }
381 
382  public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary = false)
383  {
384  Skill skill = null;
385  Color? jobColor = null;
386  if (characterInfo.Job != null)
387  {
388  skill = characterInfo.Job?.PrimarySkill ?? characterInfo.Job.GetSkills().OrderByDescending(s => s.Level).FirstOrDefault();
389  jobColor = characterInfo.Job.Prefab.UIColor;
390  }
391 
392  GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement")
393  {
394  UserData = new InfoSkill(characterInfo, skill?.Level ?? 0.0f)
395  };
396  GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
397  {
398  Stretch = true
399  };
400 
401  float portraitWidth = (0.8f * mainGroup.Rect.Height) / mainGroup.Rect.Width;
402  var icon = new GUICustomComponent(new RectTransform(new Vector2(portraitWidth, 0.8f), mainGroup.RectTransform),
403  onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2()))
404  {
405  CanBeFocused = false
406  };
407 
408  GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)) { CanBeFocused = false };
409  GUILayoutGroup nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { CanBeFocused = false };
410  GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameGroup.RectTransform),
411  listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name,
412  textColor: jobColor, textAlignment: Alignment.BottomLeft)
413  {
414  CanBeFocused = false
415  };
416  nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
417 
418  GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform),
419  characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft)
420  {
421  CanBeFocused = false
422  };
423  if (!characterInfo.MinReputationToHire.factionId.IsEmpty)
424  {
425  var faction = campaign.Factions.Find(f => f.Prefab.Identifier == characterInfo.MinReputationToHire.factionId);
426  if (faction != null)
427  {
428  jobBlock.TextColor = faction.Prefab.IconColor;
429  }
430  }
431  var fullJobText = jobBlock.Text;
432  jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width);
433  if (jobBlock.Text != fullJobText)
434  {
435  jobBlock.ToolTip = fullJobText;
436  jobBlock.CanBeFocused = true;
437  }
438  float width = 0.6f / 3;
439  if (characterInfo.Job != null && skill != null)
440  {
441  GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true);
442  float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width;
443  GUIImage skillIcon = new GUIImage(new RectTransform(Vector2.One, skillGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), skill.Icon, scaleToFit: true)
444  {
445  CanBeFocused = false
446  };
447  if (jobColor.HasValue) { skillIcon.Color = jobColor.Value; }
448  new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 1.0f), skillGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.CenterLeft)
449  {
450  CanBeFocused = false
451  };
452  }
453 
454  if (!hideSalary)
455  {
456  if (listBox != crewList)
457  {
458  new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
459  TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)),
460  textAlignment: Alignment.Center)
461  {
462  CanBeFocused = false
463  };
464  }
465  else
466  {
467  // Just a bit of padding to make list layouts similar
468  new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false };
469  }
470  }
471 
472  if (listBox == hireableList)
473  {
474  var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton")
475  {
476  ToolTip = TextManager.Get("hirebutton"),
477  ClickSound = GUISoundType.Cart,
478  UserData = characterInfo,
479  Enabled = CanHire(characterInfo) && !HiringNewCharacter,
480  OnClicked = (b, o) => AddPendingHire(o as CharacterInfo)
481  };
482  hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
483  {
484  if (HiringNewCharacter)
485  {
486  return;
487  }
488  if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
489  {
490  if (btn.Enabled)
491  {
492  btn.ToolTip = TextManager.Get("canthiremorecharacters");
493  btn.Enabled = false;
494  }
495  }
496  else if (!btn.Enabled)
497  {
498  btn.ToolTip = string.Empty;
499  btn.Enabled = CanHire(characterInfo);
500  }
501  };
502 
503  if (HiringNewCharacter)
504  {
505  bool canHire = CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo));
506  var takeoverButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton")
507  {
508  ToolTip = canHire ? TextManager.Get("hireandtakecontrol") : TextManager.Get("hireandtakecontroldisabled"),
509  ClickSound = GUISoundType.ConfirmTransaction,
510  UserData = characterInfo,
511  Enabled = canHire,
512  OnClicked = (b, o) =>
513  {
514  if (GameMain.Client is not GameClient gameClient)
515  {
516  return false;
517  }
518  Client client = gameClient.ConnectedClients.FirstOrDefault(c => c.SessionId == gameClient.SessionId);
519  if (!campaign.TryPurchase(client, HireManager.GetSalaryFor(characterInfo)))
520  {
521  return false;
522  }
523  gameClient.SendTakeOverBotRequest(characterInfo);
524  needsHireableRefresh = true;
525  campaign.ShowCampaignUI = false;
526  return true;
527  }
528  };
529  takeoverButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
530  {
531  bool canHireCurrently = HiringNewCharacter && CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo));
532  btn.ToolTip = TextManager.Get(canHireCurrently ? "hireandtakecontrol" : "hireandtakecontroldisabled");
533  btn.Visible = GameMain.GameSession is { AllowHrManagerBotTakeover: true };
534  btn.Enabled = canHireCurrently;
535  };
536  }
537  }
538  else if (listBox == pendingList)
539  {
540  new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton")
541  {
542  ClickSound = GUISoundType.Cart,
543  UserData = characterInfo,
544  Enabled = CanHire(characterInfo),
545  OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo)
546  };
547  }
548  else if (listBox == crewList && campaign != null)
549  {
551  new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementFireButton")
552  {
553  UserData = characterInfo,
554  //can't fire if there's only one character in the crew
555  Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermissionToHire,
556  OnClicked = (btn, obj) =>
557  {
558  var confirmDialog = new GUIMessageBox(
559  TextManager.Get("FireWarningHeader"),
560  TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name),
561  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
562  confirmDialog.Buttons[0].UserData = (CharacterInfo)obj;
563  confirmDialog.Buttons[0].OnClicked = FireCharacter;
564  confirmDialog.Buttons[0].OnClicked += confirmDialog.Close;
565  confirmDialog.Buttons[1].OnClicked = confirmDialog.Close;
566  return true;
567  }
568  };
569  }
570 
571  if (listBox == pendingList || listBox == crewList)
572  {
573  nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height));
574  nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
575  nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height));
576  Point size = new Point((int)(0.7f * nameBlock.Rect.Height));
577  new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false };
578  size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height);
579  new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null)
580  {
581  Enabled = CanHire(characterInfo),
582  ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel),
583  UserData = characterInfo,
584  OnClicked = CreateRenamingComponent
585  };
586  }
587 
588  bool CanHire(CharacterInfo thisCharacterInfo)
589  {
590  if (!HasPermissionToHire) { return false; }
591  return EnoughReputationToHire(thisCharacterInfo);
592  }
593 
594  return frame;
595  }
596 
597  private bool EnoughReputationToHire(CharacterInfo characterInfo)
598  {
599  if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
600  {
601  if (MathF.Round(campaign.GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
602  {
603  return false;
604  }
605  }
606  return true;
607  }
608 
609  private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo)
610  {
611  Pivot pivot = listBox == hireableList ? Pivot.TopLeft : Pivot.TopRight;
612  Point absoluteOffset = new Point(
613  pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5,
614  characterFrame.Rect.Top);
615  Point frameSize = new Point(GUI.IntScale(300), GUI.IntScale(350));
616  if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize.Y) < 0)
617  {
618  pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight;
619  absoluteOffset.Y = characterFrame.Rect.Bottom;
620  }
621  characterPreviewFrame = new GUIFrame(new RectTransform(frameSize, parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot)
622  {
623  AbsoluteOffset = absoluteOffset
624  }, style: "InnerFrame")
625  {
626  UserData = characterInfo
627  };
628  GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center))
629  {
630  RelativeSpacing = 0.01f,
631  Stretch = true
632  };
633 
634  // Character info
635  GUILayoutGroup infoGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true);
636  GUILayoutGroup infoLabelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), infoGroup.RectTransform)) { Stretch = true };
637  GUILayoutGroup infoValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), infoGroup.RectTransform)) { Stretch = true };
638  float blockHeight = 1.0f / 4;
639  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name"));
640  GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), "");
641  string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name;
642  nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width);
643 
644  if (characterInfo.HasSpecifierTags)
645  {
646  var menuCategoryVar = characterInfo.Prefab.MenuCategoryVar;
647  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar));
648  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.ReplaceVars($"[{menuCategoryVar}]")));
649  }
650  if (characterInfo.Job is Job job)
651  {
652  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("tabmenu.job"));
653  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), job.Name);
654  }
655  if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait)
656  {
657  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait"));
658  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), trait.DisplayName);
659  }
660  infoLabelGroup.Recalculate();
661  infoValueGroup.Recalculate();
662 
663  new GUIImage(new RectTransform(new Vector2(1.0f, 0.05f), mainGroup.RectTransform), "HorizontalLine");
664 
665  // Skills
666  GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true);
667  GUILayoutGroup skillNameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), skillGroup.RectTransform));
668  GUILayoutGroup skillLevelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1.0f), skillGroup.RectTransform));
669  var characterSkills = characterInfo.Job.GetSkills();
670  blockHeight = 1.0f / characterSkills.Count();
671  foreach (Skill skill in characterSkills)
672  {
673  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier), font: GUIStyle.SmallFont);
674  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right);
675  }
676 
677  if (characterInfo.MinReputationToHire.reputation > 0.0f)
678  {
679  var repStr = TextManager.GetWithVariables(
680  "campaignstore.reputationrequired",
681  ("[amount]", ((int)characterInfo.MinReputationToHire.reputation).ToString()),
682  ("[faction]", TextManager.Get("faction." + characterInfo.MinReputationToHire.factionId).Value));
683  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), mainGroup.RectTransform),
684  repStr, textColor: !EnoughReputationToHire(characterInfo) ? GUIStyle.Orange : GUIStyle.Green,
685  font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center);
686  }
687  mainGroup.Recalculate();
688  characterPreviewFrame.RectTransform.MinSize =
689  new Point(0, (int)(mainGroup.Children.Sum(c => c.Rect.Height + mainGroup.Rect.Height * mainGroup.RelativeSpacing) / mainGroup.RectTransform.RelativeSize.Y));
690  }
691 
692  private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo)
693  {
694  if (characterPreviewFrame != null && characterPreviewFrame.UserData != characterInfo)
695  {
696  characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
697  characterPreviewFrame = null;
698  }
699 
700  if (listBox == null || characterFrame == null || characterInfo == null) { return false; }
701 
702  if (characterPreviewFrame == null)
703  {
704  CreateCharacterPreviewFrame(listBox, characterFrame, characterInfo);
705  }
706 
707  return true;
708  }
709 
710  private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true)
711  {
712  if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
713  {
714  return false;
715  }
716 
717  hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo));
718  hireableList.UpdateScrollBarSize();
719  if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); }
720  CreateCharacterFrame(characterInfo, pendingList);
721  SortCharacters(pendingList, SortingMethod.JobAsc);
722  pendingList.UpdateScrollBarSize();
723  SetTotalHireCost();
724  if (createNetworkMessage) { SendCrewState(true); }
725  return true;
726  }
727 
728  private bool RemovePendingHire(CharacterInfo characterInfo, bool setTotalHireCost = true, bool createNetworkMessage = true)
729  {
730  if (PendingHires.Contains(characterInfo)) { PendingHires.Remove(characterInfo); }
731  pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo));
732  pendingList.UpdateScrollBarSize();
733 
734  // Server will reset the names to originals in multiplayer
735  if (!GameMain.IsMultiplayer) { characterInfo?.ResetName(); }
736 
737  if (campaign.Map.CurrentLocation.HireManager.AvailableCharacters.Any(info => info.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()) &&
738  hireableList.Content.Children.None(c => c.UserData is InfoSkill userData && userData.CharacterInfo.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()))
739  {
740  CreateCharacterFrame(characterInfo, hireableList);
741  SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData);
742  hireableList.UpdateScrollBarSize();
743  }
744 
745  if (setTotalHireCost) { SetTotalHireCost(); }
746  if (createNetworkMessage) { SendCrewState(true); }
747  return true;
748  }
749 
750  private bool RemoveAllPendingHires(bool createNetworkMessage = true)
751  {
752  pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire(((InfoSkill)c.UserData).CharacterInfo, setTotalHireCost: false, createNetworkMessage));
753  SetTotalHireCost();
754  return true;
755  }
756 
757  private void SetTotalHireCost()
758  {
759  if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; }
760  var infos = pendingList.Content.Children.Select(static c => ((InfoSkill)c.UserData).CharacterInfo).ToArray();
761  int total = HireManager.GetSalaryFor(infos);
762  totalBlock.Text = TextManager.FormatCurrency(total);
763  bool enoughMoney = campaign == null || campaign.CanAfford(total);
764  totalBlock.TextColor = enoughMoney ? Color.White : Color.Red;
765  validateHiresButton.Enabled = enoughMoney && HasPermissionToHire && pendingList.Content.RectTransform.Children.Any();
766  }
767 
768  public bool ValidateHires(List<CharacterInfo> hires, bool takeMoney = true, bool createNetworkEvent = false, bool createNotification = true)
769  {
770  if (hires == null || hires.None()) { return false; }
771 
772  List<CharacterInfo> nonDuplicateHires = new List<CharacterInfo>();
773  hires.ForEach(hireInfo =>
774  {
775  if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName()))
776  {
777  nonDuplicateHires.Add(hireInfo);
778  }
779  });
780 
781  if (nonDuplicateHires.None()) { return false; }
782 
783  if (takeMoney)
784  {
785  int total = HireManager.GetSalaryFor(nonDuplicateHires);
786  if (!campaign.CanAfford(total)) { return false; }
787  }
788 
789  bool atLeastOneHired = false;
790  foreach (CharacterInfo ci in nonDuplicateHires)
791  {
792  if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, takeMoney: takeMoney))
793  {
794  atLeastOneHired = true;
795  }
796  else
797  {
798  break;
799  }
800  }
801 
802  if (atLeastOneHired)
803  {
804  UpdateLocationView(campaign.Map.CurrentLocation, true);
805  SelectCharacter(null, null, null);
806  if (createNotification)
807  {
808  var dialog = new GUIMessageBox(
809  TextManager.Get("newcrewmembers"),
810  TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
811  new LocalizedString[] { TextManager.Get("Ok") });
812  dialog.Buttons[0].OnClicked += dialog.Close;
813  }
814  }
815 
816  if (createNetworkEvent)
817  {
818  SendCrewState(true, validateHires: true);
819  }
820 
821  return false;
822  }
823 
824  private bool CreateRenamingComponent(GUIButton button, object userData)
825  {
826  if (!HasPermissionToHire || userData is not CharacterInfo characterInfo) { return false; }
827  var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center),
828  style: "OuterGlow", color: Color.Black * 0.7f);
829  var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center)
830  {
831  MaxSize = new Point(400, 300).Multiply(GUI.Scale)
832  });
833  var layoutGroup = new GUILayoutGroup(new RectTransform((frame.Rect.Size - GUIStyle.ItemFrameMargin).Multiply(new Vector2(0.75f, 1.0f)), frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
834  {
835  RelativeSpacing = 0.02f,
836  Stretch = true
837  };
838  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true);
839  var groupElementSize = new Vector2(1.0f, 0.25f);
840  var nameBox = new GUITextBox(new RectTransform(groupElementSize, layoutGroup.RectTransform))
841  {
842  MaxTextLength = Client.MaxNameLength
843  };
844  new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("confirm"))
845  {
846  OnClicked = (button, userData) =>
847  {
848  if (RenameCharacter(characterInfo, nameBox.Text?.Trim()))
849  {
850  parentComponent.RemoveChild(outerGlowFrame);
851  return true;
852  }
853  else
854  {
855  nameBox.Flash(color: Color.Red);
856  return false;
857  }
858 
859  }
860  };
861  new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("cancel"))
862  {
863  OnClicked = (button, userData) =>
864  {
865  parentComponent.RemoveChild(outerGlowFrame);
866  return true;
867  }
868  };
869  layoutGroup.Recalculate();
870  return true;
871  }
872 
873  public bool RenameCharacter(CharacterInfo characterInfo, string newName)
874  {
875  if (characterInfo == null || string.IsNullOrEmpty(newName)) { return false; }
876  if (newName == characterInfo.Name) { return false; }
878  {
879  SendCrewState(false, renameCharacter: (characterInfo, newName));
880  }
881  else
882  {
883  var crewComponent = crewList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo);
884  if (crewComponent != null)
885  {
886  crewList.Content.RemoveChild(crewComponent);
887  campaign.CrewManager.RenameCharacter(characterInfo, newName);
888  CreateCharacterFrame(characterInfo, crewList);
889  SortCharacters(crewList, SortingMethod.JobAsc);
890  }
891  else
892  {
893  var pendingComponent = pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo);
894  if (pendingComponent != null)
895  {
896  pendingList.Content.RemoveChild(pendingComponent);
897  campaign.Map.CurrentLocation.HireManager.RenameCharacter(characterInfo, newName);
898  CreateCharacterFrame(characterInfo, pendingList);
899  SortCharacters(pendingList, SortingMethod.JobAsc);
900  SetTotalHireCost();
901  }
902  else
903  {
904  return false;
905  }
906  }
907  }
908  return true;
909  }
910 
911  private bool FireCharacter(GUIButton button, object selection)
912  {
913  if (selection is not CharacterInfo characterInfo) { return false; }
914 
915  campaign.CrewManager.FireCharacter(characterInfo);
916  SelectCharacter(null, null, null);
917  UpdateCrew();
918 
919  SendCrewState(false, firedCharacter: characterInfo);
920  return false;
921  }
922 
923  public void Update()
924  {
925  if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y)
926  {
927  CreateUI();
928  UpdateLocationView(campaign.Map.CurrentLocation, false);
929  }
930  else
931  {
932  playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement);
933  }
934 
935  // When showing this window to someone hiring a new character, the right side panels aren't needed
936  pendingAndCrewGroup.Visible = !HiringNewCharacter;
937 
938  if (needsHireableRefresh)
939  {
940  RefreshCrewFrames(hireableList);
941  if (sortingDropDown?.SelectedItemData != null)
942  {
943  SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData);
944  }
945  needsHireableRefresh = false;
946  }
947 
948  (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn);
949  if (highlightedFrame != null && highlightedInfo != null)
950  {
951  if (characterPreviewFrame == null || highlightedInfo != characterPreviewFrame.UserData)
952  {
953  GUIComponent component = GUI.MouseOn;
954  GUIListBox listBox = null;
955  do
956  {
957  if (component.Parent is GUIListBox)
958  {
959  listBox = component.Parent as GUIListBox;
960  break;
961  }
962  else if (component.Parent != null)
963  {
964  component = component.Parent;
965  }
966  else
967  {
968  break;
969  }
970  } while (listBox == null);
971 
972  if (listBox != null)
973  {
974  SelectCharacter(listBox, highlightedFrame as GUIFrame, highlightedInfo);
975  }
976  }
977  else
978  {
979  // TODO: Reposition the current preview panel if necessary
980  // Could happen if we scroll a list while hovering?
981  }
982  }
983  else if (characterPreviewFrame != null)
984  {
985  characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
986  characterPreviewFrame = null;
987  }
988 
989  static (GUIComponent GuiComponent, CharacterInfo CharacterInfo) FindHighlightedCharacter(GUIComponent c)
990  {
991  if (c == null)
992  {
993  return default;
994  }
995  if (c.UserData is InfoSkill highlightedData)
996  {
997  return (c, highlightedData.CharacterInfo);
998  }
999  if (c.Parent != null)
1000  {
1001  if (c.Parent is GUIListBox)
1002  {
1003  return default;
1004  }
1005  return FindHighlightedCharacter(c.Parent);
1006  }
1007  return default;
1008  }
1009  }
1010 
1011  public void SetPendingHires(List<UInt16> characterInfos, Location location)
1012  {
1013  List<CharacterInfo> oldHires = PendingHires.ToList();
1014  foreach (CharacterInfo pendingHire in oldHires)
1015  {
1016  RemovePendingHire(pendingHire, createNetworkMessage: false);
1017  }
1018  PendingHires.Clear();
1019  foreach (UInt16 identifier in characterInfos)
1020  {
1021  CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.ID == identifier);
1022  if (match != null)
1023  {
1024  AddPendingHire(match, createNetworkMessage: false);
1025  System.Diagnostics.Debug.Assert(PendingHires.Contains(match));
1026  }
1027  else
1028  {
1029  DebugConsole.ThrowError("Received a hire that doesn't exist.");
1030  }
1031  }
1032  }
1033 
1041  public void SendCrewState(bool updatePending, (CharacterInfo info, string newName) renameCharacter = default, CharacterInfo firedCharacter = null, bool validateHires = false)
1042  {
1043  if (campaign is MultiPlayerCampaign)
1044  {
1045  IWriteMessage msg = new WriteOnlyMessage();
1046  msg.WriteByte((byte)ClientPacketHeader.CREW);
1047 
1048  msg.WriteBoolean(updatePending);
1049  if (updatePending)
1050  {
1051  msg.WriteUInt16((ushort)PendingHires.Count);
1052  foreach (CharacterInfo pendingHire in PendingHires)
1053  {
1054  msg.WriteUInt16(pendingHire.ID);
1055  }
1056  }
1057 
1058  msg.WriteBoolean(validateHires);
1059 
1060  bool validRenaming = renameCharacter.info != null && !string.IsNullOrEmpty(renameCharacter.newName);
1061  msg.WriteBoolean(validRenaming);
1062  if (validRenaming)
1063  {
1064  msg.WriteUInt16(renameCharacter.info.ID);
1065  msg.WriteString(renameCharacter.newName);
1066  bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.ID == renameCharacter.info.ID) ?? false;
1067  msg.WriteBoolean(existingCrewMember);
1068  }
1069 
1070  msg.WriteBoolean(firedCharacter != null);
1071  if (firedCharacter != null)
1072  {
1073  msg.WriteUInt16(firedCharacter.ID);
1074  }
1075 
1076  GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1077  }
1078  }
1079  }
1080 }
static bool AllowedToManageCampaign(ClientPermissions permissions)
There is a server-side implementation of the method in MultiPlayerCampaign
bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney=true, Client client=null)
static ? PlayerBalanceElement UpdateBalanceElement(PlayerBalanceElement? playerBalanceElement)
Definition: CampaignUI.cs:717
CampaignMode Campaign
Definition: CampaignUI.cs:40
Stores information about the Character that is needed between rounds in the menu etc....
CharacterInfo(Identifier speciesName, string name="", string originalName="", Either< Job, JobPrefab > jobOrJobPrefab=null, int variant=0, Rand.RandSync randSync=Rand.RandSync.Unsynced, Identifier npcIdentifier=default)
void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize)
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
void SetHireables(Location location, List< CharacterInfo > availableHires)
bool ValidateHires(List< CharacterInfo > hires, bool takeMoney=true, bool createNetworkEvent=false, bool createNotification=true)
bool RenameCharacter(CharacterInfo characterInfo, string newName)
GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary=false)
void SetPendingHires(List< UInt16 > characterInfos, Location location)
CrewManagement(CampaignUI campaignUI, GUIComponent parentComponent)
void SendCrewState(bool updatePending,(CharacterInfo info, string newName) renameCharacter=default, CharacterInfo firedCharacter=null, bool validateHires=false)
Notify the server of crew changes
void RenameCharacter(CharacterInfo characterInfo, string newName)
override bool Enabled
Definition: GUIButton.cs:27
GUITextBlock TextBlock
Definition: GUIButton.cs:11
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual RichString ToolTip
virtual Rectangle Rect
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIComponent that can be used to render custom content on the UI
GUIComponent AddItem(LocalizedString text, object userData=null, LocalizedString toolTip=null, Color? color=null, Color? textColor=null)
Definition: GUIDropDown.cs:247
override void RemoveChild(GUIComponent child)
Definition: GUIListBox.cs:1249
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
override GUIFont Font
Definition: GUITextBlock.cs:66
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static int GraphicsHeight
Definition: GameMain.cs:168
static bool IsMultiplayer
Definition: GameMain.cs:35
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static int GetSalaryFor(IReadOnlyCollection< CharacterInfo > hires)
Definition: HireManager.cs:24
List< CharacterInfo > PendingHires
Definition: HireManager.cs:10
void RenameCharacter(CharacterInfo characterInfo, string newName)
Definition: HireManager.cs:88
List< CharacterInfo > AvailableCharacters
Definition: HireManager.cs:9
Skill PrimarySkill
Definition: Job.cs:22
IEnumerable< Skill > GetSkills()
Definition: Job.cs:84
LocalizedString Name
Definition: Job.cs:14
JobPrefab Prefab
Definition: Job.cs:18
HireManager HireManager
Definition: Location.cs:535
IEnumerable< CharacterInfo > GetHireableCharacters()
Definition: Location.cs:1117
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
static readonly LocalizedString PrimaryMouseLabel
void Resize(Point absoluteSize, bool resizeChildren=true)
IEnumerable< RectTransform > Children
void SortChildren(Comparison< RectTransform > comparison)
Point?? MinSize
Min size in pixels. Does not affect scaling.
static readonly NamedEvent< Reputation > OnAnyReputationValueChanged
Definition: Reputation.cs:122
Sprite Icon
Definition: Skill.cs:37
float Level
Definition: Skill.cs:19
GUISoundType
Definition: GUI.cs:21