[tweak] UI Tweaks (#1001)

* - tweak: update StyleSheetify

* - add: flexbox

* - fix: size of flexbox in launchergui

* - tweak: Profile editor: start.

* - add: categories

* - tweak: help me please with this shi... loadouts

* - fix: container path think

* - tweak: thinks for optimisation

* - add: group selection for loadoutpicker

* - tweak: change position of preview

* - add: reason text

* - fix: Кролькины фиксы

* - fix: кролькины фиксы ч.2

* - fix: кролькины фиксы ч.3

* - кролькины фиксы - финал

* - fix: Ворчливого дедушкины фиксы, удаление старого барахла и пометка wwdp

* - tweak: some ui change for LoadoutCategories and LoadoutEntry

* - ворчливый дед фиксы ч.2

* - fix: очередные кролькины фиксы

* - add: loadout prototype validation

* - fix: description read from edit field
This commit is contained in:
Cinkafox
2026-01-05 00:33:01 +03:00
committed by GitHub
parent d57339f670
commit b2d255cdd2
36 changed files with 1808 additions and 782 deletions

View File

@@ -6,7 +6,7 @@
<!-- Markings -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer HorizontalExpand="True">
<OptionButton Name="CMarkingCategoryButton" StyleClasses="OpenBoth" />
<OptionButton Name="CMarkingCategoryButton" StyleClasses="OpenBoth" Access="Public" /> <!-- WWDP EDIT -->
<Label Name="CMarkingPoints" Text="uwu" Margin="8 0" />
<LineEdit Name="CMarkingSearch" PlaceHolder="{Loc 'markings-search'}" HorizontalExpand="True" />
<Button Name="ResetButton" Text="{Loc 'ui-options-bind-reset'}" StyleClasses="OpenLeft" />

View File

@@ -110,6 +110,18 @@ public sealed partial class MarkingPicker : Control
Populate(CMarkingSearch.Text);
}
// WWDP EDIT START
public bool Select(MarkingCategories category)
{
_selectedMarkingCategory = category;
if (!CMarkingCategoryButton.TrySelectId(_markingCategories.IndexOf(_selectedMarkingCategory)))
return false;
Populate(CMarkingSearch.Text);
UpdatePoints();
return true;
}
// WWDP EDIT END
public MarkingPicker()
{
RobustXamlLoader.Load(this);
@@ -126,6 +138,9 @@ public sealed partial class MarkingPicker : Control
Populate(CMarkingSearch.Text);
}
public bool IsCategoryValid(MarkingCategories category) =>
!_ignoreCategories.Contains(category) && GetMarkings(category).Count > 0; // WWDP EDIT
private void SetupCategoryButtons()
{
CMarkingCategoryButton.Clear();
@@ -208,6 +223,7 @@ public sealed partial class MarkingPicker : Control
{
Texture = marking.Sprites[0].DirFrame0().TextureFor(
Enum.TryParse<Direction>(marking.PreviewDirection, out var dir) ? dir : Direction.South),
TextureScale = new Vector2(1.5f) // WWDP EDIT
},
},
};

View File

@@ -2,11 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:parallax="clr-namespace:Content.Client.Parallax"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:controlsWhite="clr-namespace:Content.Client._White.UserInterface.Controls">
<parallax:ParallaxControl />
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
<PanelContainer StyleClasses="MainPanel" />
<BoxContainer Orientation="Horizontal" MinSize="300 200" Margin="10">
<controlsWhite:FlexBox MinSize="300 200" Margin="10">
<BoxContainer Orientation="Vertical" Margin="0,0,5,0">
<BoxContainer Orientation="Horizontal" Margin="0 5 0 8">
<Label Margin="8 0 0 0" Text="{Loc 'connecting-title'}"
@@ -85,7 +86,7 @@
</BoxContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</controlsWhite:FlexBox>
</Control>
<!-- Bottom window for tips -->

View File

@@ -56,13 +56,6 @@ namespace Content.Client.Lobby
_voteManager.SetPopupContainer(Lobby.VoteContainer);
LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide);
var lobbyNameCvar = _cfg.GetCVar(CCVars.ServerLobbyName);
var serverName = _baseClient.GameInfo?.ServerName ?? string.Empty;
Lobby.ServerName.Text = string.IsNullOrEmpty(lobbyNameCvar)
? Loc.GetString("ui-lobby-title", ("serverName", serverName))
: lobbyNameCvar;
var width = _cfg.GetCVar(CCVars.ServerLobbyRightPanelWidth);
Lobby.RightSide.SetWidth = width;

View File

@@ -115,9 +115,6 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
if (obj.WasModified<TraitPrototype>())
_profileEditor.UpdateTraits(null, true);
if (obj.WasModified<LoadoutPrototype>())
_profileEditor.UpdateLoadouts(null, true);
}
private void PreferencesDataLoaded()

View File

@@ -35,7 +35,27 @@
</PanelContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" SeparationOverride="0">
<ScrollContainer MinSize="325 0" Margin="5 5 0 0">
<BoxContainer Name="Characters" Orientation="Vertical" />
<!-- WWDP EDIT START -->
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer VerticalExpand="True" Name="Characters" Orientation="Vertical" />
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center">
<SpriteView Name="CharacterSpriteView" Access="Public" OverrideDirection="South" Scale="7 7" />
<BoxContainer HorizontalAlignment="Center">
<Button Name="PreviewRotateLeftButton" StyleClasses="OpenRight" >
<TextureRect TexturePath="/Textures/Interface/Default/left_arrow.svg.192dpi.png" Stretch="KeepAspectCentered"/>
</Button>
<Button Name="PreviewRotateRightButton" StyleClasses="OpenLeft">
<TextureRect TexturePath="/Textures/Interface/Default/right_arrow.svg.192dpi.png" Stretch="KeepAspectCentered"/>
</Button>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- WWDP EDIT END -->
</ScrollContainer>
<PanelContainer MinSize="2 0">
<PanelContainer.PanelOverride>

View File

@@ -63,6 +63,18 @@ namespace Content.Client.Lobby.UI
StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
//WWDP EDIT START
profileEditor.CharacterSpriteView = CharacterSpriteView;
CharacterSpriteView.SetEntity(profileEditor.PreviewDummy);
PreviewRotateRightButton.OnPressed += args =>
CharacterSpriteView.OverrideDirection =
(Direction)(((int)(CharacterSpriteView.OverrideDirection ?? Direction.South) + 2) % 8);
PreviewRotateLeftButton.OnPressed += args =>
CharacterSpriteView.OverrideDirection =
(Direction)(((int)(CharacterSpriteView.OverrideDirection ?? Direction.South) + 6) % 8);
//WWDP EDIT END
_cfg.OnValueChanged(CCVars.SeeOwnNotes, p => AdminRemarksButton.Visible = p, true);
}

View File

@@ -5,6 +5,7 @@
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client._White.UserInterface.Controls"
xmlns:loadouts="clr-namespace:Content.Client._White.Loadouts"
HorizontalExpand="True">
<!-- Left side -->
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
@@ -180,6 +181,22 @@
<Label Name="WeightLabel" />
</BoxContainer>
</ui:AlternatingBGContainer>
<!-- WD EDIT START -->
<Control Margin="5"/>
<BoxContainer HorizontalExpand="True">
<BoxContainer HorizontalExpand="True" Orientation="Vertical">
<Label Text="{Loc 'humanoid-profile-editor-markings-tab'}" />
<Control Margin="5"/>
<humanoid:MarkingPicker Name="Markings" HorizontalExpand="True" HorizontalAlignment="Stretch" Access="Public"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True" Orientation="Vertical">
<Label Text="{Loc 'humanoid-profile-editor-loadouts-tab'}" />
<Control Margin="5"/>
<loadouts:LoadoutPicker Name="Loadouts" HorizontalExpand="True" HorizontalAlignment="Stretch" Access="Public"/>
</BoxContainer>
</BoxContainer>
<Control Margin="5"/>
<!-- WD EDIT END -->
<!-- Skin -->
<BoxContainer Margin="0 4 0 4" HorizontalExpand="True" Orientation="Vertical">
<Label Text="{Loc 'humanoid-profile-editor-skin-color-label'}" />
@@ -277,52 +294,8 @@
<ui:NeoTabContainer Name="TraitsTabs" VerticalExpand="True" SeparatorMargin="0" />
</BoxContainer>
<BoxContainer Name="LoadoutsTab" HorizontalExpand="True" Orientation="Vertical">
<!-- Loadouts -->
<Label Name="LoadoutPointsLabel" HorizontalAlignment="Stretch" Align="Center" />
<ProgressBar Name="LoadoutPointsBar" MaxValue="1" Value="1" MaxHeight="8" Margin="0 5" />
<BoxContainer HorizontalExpand="True" Margin="0 0 0 5">
<Button
Name="LoadoutsShowUnusableButton"
Text="{Loc 'humanoid-profile-editor-loadouts-show-unusable-button'}"
ToolTip="{Loc 'humanoid-profile-editor-loadouts-show-unusable-button-tooltip'}"
ToggleMode="True"
HorizontalAlignment="Stretch"
HorizontalExpand="True"
StyleClasses="OpenRight" />
<Button
Name="LoadoutsRemoveUnusableButton"
Text="You shouldn't see this"
ToolTip="{Loc 'humanoid-profile-editor-loadouts-remove-unusable-button-tooltip'}"
HorizontalAlignment="Stretch"
HorizontalExpand="True"
StyleClasses="OpenLeft" />
</BoxContainer>
<ui:NeoTabContainer Name="LoadoutsTabs" VerticalExpand="True" SeparatorMargin="0" />
</BoxContainer>
<BoxContainer Name="MarkingsTab" HorizontalExpand="True" Orientation="Vertical">
<ScrollContainer HorizontalExpand="True" HScrollEnabled="True" VerticalExpand="True" VScrollEnabled="True">
<!-- Markings -->
<humanoid:MarkingPicker Name="Markings" IgnoreCategories="Hair,FacialHair" />
</ScrollContainer>
</BoxContainer>
</ui:NeoTabContainer>
</PanelContainer>
</BoxContainer>
<!-- Right side -->
<!-- Sprite View -->
<PanelContainer VerticalExpand="True" VerticalAlignment="Center" StyleClasses="PanelBackground"> <!-- WD EDIT -->
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2f2f2f" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<SpriteView Name="SpriteViewS" OverrideDirection="South" Scale="4 4" />
<SpriteView Name="SpriteViewN" OverrideDirection="North" Scale="4 4" />
<SpriteView Name="SpriteViewE" OverrideDirection="East" Scale="4 4" />
<SpriteView Name="SpriteViewW" OverrideDirection="West" Scale="4 4" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -106,7 +106,6 @@ namespace Content.Client.Lobby.UI
private Dictionary<Button, ConfirmationData> _confirmationData = new();
private List<TraitPreferenceSelector> _traitPreferences = new();
private int _traitCount;
private HashSet<LoadoutPreferenceSelector> _loadoutPreferences = new();
private bool _customizePronouns;
private bool _customizeStationAiName;
@@ -145,6 +144,8 @@ namespace Content.Client.Lobby.UI
private const string MimeNames = "MimeNames";
private const string Uncategorized = "Uncategorized";
public SpriteView? CharacterSpriteView;
// WD EDIT END
public HumanoidProfileEditor(
@@ -597,31 +598,8 @@ namespace Content.Client.Lobby.UI
#endregion
#region Loadouts
// Set up the loadouts tab
LoadoutsTab.Orphan();
CTabContainer.AddTab(LoadoutsTab, Loc.GetString("humanoid-profile-editor-loadouts-tab"));
_loadoutPreferences = new();
// Show/Hide the loadouts tab if they ever get enabled/disabled
var loadoutsEnabled = cfgManager.GetCVar(CCVars.GameLoadoutsEnabled);
CTabContainer.SetTabVisible(4, loadoutsEnabled);
ShowLoadouts.Visible = loadoutsEnabled;
cfgManager.OnValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
LoadoutsShowUnusableButton.OnToggled += args => UpdateLoadouts(args.Pressed);
LoadoutsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableLoadouts();
UpdateLoadouts(false);
#endregion
#region Markings
MarkingsTab.Orphan();
CTabContainer.AddTab(MarkingsTab, Loc.GetString("humanoid-profile-editor-markings-tab"));
Markings.OnMarkingAdded += OnMarkingChange;
Markings.OnMarkingRemoved += OnMarkingChange;
Markings.OnMarkingColorChange += OnMarkingChange;
@@ -640,6 +618,7 @@ namespace Content.Client.Lobby.UI
UpdateSpeciesGuidebookIcon();
ReloadPreview();
InitializeCharacterMenu(); // WWDP EDIT
IsDirty = false;
}
@@ -944,10 +923,7 @@ namespace Content.Client.Lobby.UI
return;
PreviewDummy = _controller.LoadProfileEntity(Profile, null, ShowClothes.Pressed, ShowLoadouts.Pressed);
SpriteViewS.SetEntity(PreviewDummy);
SpriteViewN.SetEntity(PreviewDummy);
SpriteViewE.SetEntity(PreviewDummy);
SpriteViewW.SetEntity(PreviewDummy);
CharacterSpriteView?.SetEntity(PreviewDummy); // WWDP EDIT
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
@@ -1009,6 +985,8 @@ namespace Content.Client.Lobby.UI
UpdateEyePickers();
UpdateSaveButton();
UpdateMarkings();
UpdateLoadouts(); // WD EDIT
CheckpointLoadouts(); // WD EDIT
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
@@ -1048,7 +1026,6 @@ namespace Content.Client.Lobby.UI
}
TraitsTabs.UpdateTabMerging();
LoadoutsTabs.UpdateTabMerging();
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
@@ -1893,10 +1870,7 @@ namespace Content.Client.Lobby.UI
else // Whelp, the fixture doesn't exist, guesstimate it instead
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) 71));
SpriteViewS.InvalidateMeasure();
SpriteViewN.InvalidateMeasure();
SpriteViewE.InvalidateMeasure();
SpriteViewW.InvalidateMeasure();
CharacterSpriteView?.InvalidateMeasure();
}
private void UpdateHairPickers()
@@ -2157,7 +2131,6 @@ namespace Content.Client.Lobby.UI
{
foreach (var tab in TraitsTabs.TakenIds)
TraitsTabs.RemoveTab(tab);
_loadoutPreferences.Clear();
}
@@ -2374,6 +2347,23 @@ namespace Content.Client.Lobby.UI
#region Functions
private BoxContainer? FindCategory(string id, NeoTabContainer parent)
{
BoxContainer? match = null;
if(parent.TryFindTabByAlias(id, out var tabId))
match = parent.GetControl<BoxContainer>(tabId);
if (match != null)
return match;
foreach (var subcategory in parent.GetControls<NeoTabContainer>())
match ??= FindCategory(id, subcategory);
return match;
}
private Dictionary<string, object> CreateTree(List<TraitCategoryPrototype> cats)
{
var tree = new Dictionary<string, object>();
@@ -2421,385 +2411,6 @@ namespace Content.Client.Lobby.UI
#endregion
#region Loadouts
#region Updates
private void UpdateLoadoutPreferences()
{
var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
LoadoutPointsBar.Value = points;
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
foreach (var preferenceSelector in _loadoutPreferences)
{
var loadoutId = preferenceSelector.Loadout.ID;
var loadoutPreference = Profile?.LoadoutPreferences.FirstOrDefault(l => l.LoadoutName == loadoutId) ?? preferenceSelector.Preference;
var preference = new LoadoutPreference(
loadoutPreference.LoadoutName,
loadoutPreference.CustomName,
loadoutPreference.CustomDescription,
loadoutPreference.CustomContent, // WD EDIT
loadoutPreference.CustomColorTint,
loadoutPreference.CustomHeirloom)
{ Selected = loadoutPreference.Selected };
preferenceSelector.Preference = preference;
if (preference.Selected)
{
points -= preferenceSelector.Loadout.Cost;
LoadoutPointsBar.Value = points;
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", LoadoutPointsBar.MaxValue));
}
}
// Set the remove unusable button's label to have the correct amount of unusable loadouts
LoadoutsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-loadouts-remove-unusable-button",
("count", _loadouts
.Where(l => _loadoutPreferences
.Where(lps => lps.Preference.Selected).Select(lps => lps.Loadout).Contains(l.Key))
.Count(l => !l.Value
|| !_loadoutPreferences.First(lps => lps.Loadout == l.Key).Wearable)));
AdminUIHelpers.RemoveConfirm(LoadoutsRemoveUnusableButton, _confirmationData);
IsDirty = true;
ReloadProfilePreview();
}
private Dictionary<LoadoutPrototype, bool> _loadouts = new();
private Dictionary<string, EntityUid> _dummyLoadouts = new();
public void UpdateLoadouts(bool? showUnusable = null, bool reload = false)
{
showUnusable ??= LoadoutsShowUnusableButton.Pressed;
// Reset loadout points so you don't get -14 points or something for no reason
var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
LoadoutPointsBar.MaxValue = points;
LoadoutPointsBar.Value = points;
// Reset the whole UI and delete caches
if (reload)
{
foreach (var tab in LoadoutsTabs.TakenIds)
LoadoutsTabs.RemoveTab(tab);
foreach (var uid in _dummyLoadouts)
_entManager.QueueDeleteEntity(uid.Value);
_loadoutPreferences.Clear();
}
// Get the highest priority job to use for loadout filtering
var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
_loadouts.Clear();
foreach (var loadout in _prototypeManager.EnumeratePrototypes<LoadoutPrototype>())
{
var usable = _characterRequirementsSystem.CheckRequirementsValid(
loadout.Requirements,
highJob ?? new JobPrototype(),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
loadout,
_entManager,
_prototypeManager,
_cfgManager,
out _
);
_loadouts.Add(loadout, usable);
var list = _loadoutPreferences.ToList();
if (list.FindIndex(lps => lps.Loadout.ID == loadout.ID) is not (not -1 and var i))
continue;
var selector = list[i];
UpdateSelector(selector, usable);
}
if (_loadouts.Count == 0)
{
LoadoutsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-loadouts-no-loadouts") },
Loc.GetString("loadout-category-Uncategorized"));
return;
}
if (!LoadoutsTabs.TryFindTabByAlias(Uncategorized, out var id))
{
var uncategorizedB = new BoxContainer
{
Name = Uncategorized,
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
// I hate ScrollContainers
Children =
{
new ScrollContainer
{
HScrollEnabled = false,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
},
},
},
},
};
id = LoadoutsTabs.AddTab(uncategorizedB, Loc.GetString("loadout-category-Uncategorized"));
LoadoutsTabs.SetTabAlias(id, Uncategorized);
}
var uncategorized = LoadoutsTabs.GetControl<BoxContainer>(id)!;
// Create a Dictionary/tree of categories and subcategories
var cats = CreateTree(_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>()
.Where(c => c.Root)
.OrderBy(c => Loc.GetString($"loadout-category-{c.ID}"))
.ToList());
var categories = new Dictionary<string, object>();
foreach (var (key, value) in cats)
categories.Add(key, value);
// Create the UI elements for the category tree
CreateCategoryUI(categories, LoadoutsTabs);
// Fill categories with loadouts
foreach (var (loadout, usable) in _loadouts
.OrderBy(l => l.Key.ID)
.ThenBy(l => Loc.GetString($"loadout-name-{l.Key.ID}"))
.ThenBy(l => l.Key.Cost))
{
if (_loadoutPreferences.Select(lps => lps.Loadout.ID).Contains(loadout.ID))
{
var first = _loadoutPreferences.First(lps => lps.Loadout.ID == loadout.ID);
var prof = Profile?.LoadoutPreferences.FirstOrDefault(lp => lp.LoadoutName == loadout.ID);
first.Preference = new(loadout.ID, prof?.CustomName, prof?.CustomDescription, prof?.CustomContent, prof?.CustomColorTint, prof?.CustomHeirloom); // WD EDIT
UpdateSelector(first, usable);
continue;
}
var selector = new LoadoutPreferenceSelector(
loadout, highJob ?? new JobPrototype(),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(), ref _dummyLoadouts,
_entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements)
{ Preference = new(loadout.ID) };
UpdateSelector(selector, usable);
AddSelector(selector);
// Look for an existing category tab
var match = FindCategory(loadout.Category, LoadoutsTabs);
// If there is no category put it in Uncategorized (this shouldn't happen)
(match ?? uncategorized).Children.First().Children.First().AddChild(selector);
}
// Hide any empty tabs
HideEmptyTabs(_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>().ToList());
UpdateLoadoutPreferences();
return;
void UpdateSelector(LoadoutPreferenceSelector selector, bool usable)
{
selector.Valid = usable;
selector.ShowUnusable = showUnusable.Value;
foreach (var item in selector.Loadout.Items)
{
if (_dummyLoadouts.TryGetValue(selector.Loadout.ID + selector.Loadout.Items.IndexOf(item), out var entity)
&& _entManager.GetComponent<MetaDataComponent>(entity).EntityPrototype!.ID == item)
{
if (!_entManager.HasComponent<ClothingComponent>(entity))
{
selector.Wearable = true;
continue;
}
selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
continue;
}
entity = _entManager.SpawnEntity(item, MapCoordinates.Nullspace);
_dummyLoadouts[selector.Loadout.ID + selector.Loadout.Items.IndexOf(item)] = entity;
if (!_entManager.HasComponent<ClothingComponent>(entity))
{
selector.Wearable = true;
continue;
}
selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
}
}
void CreateCategoryUI(Dictionary<string, object> tree, NeoTabContainer parent)
{
foreach (var (key, value) in tree)
{
// If the category's container exists already, ignore it
if (parent.TryFindTabByAlias(key, out _))
continue;
// If the value is a list of LoadoutPrototypes, create a final tab for them
if (value is List<LoadoutPrototype>)
{
var category = new BoxContainer
{
Name = key,
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new ScrollContainer
{
HScrollEnabled = false,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
},
},
},
},
};
var catId = parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
parent.SetTabAlias(catId, key);
}
// If the value is a dictionary, create a new tab for it and recursively call this function to fill it
else
{
var category = new NeoTabContainer
{
Name = key,
HorizontalExpand = true,
VerticalExpand = true,
SeparatorMargin = new Thickness(0),
};
var catId = parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
parent.SetTabAlias(catId, key);
CreateCategoryUI((Dictionary<string, object>) value, category);
}
}
}
void AddSelector(LoadoutPreferenceSelector selector)
{
_loadoutPreferences.Add(selector);
selector.PreferenceChanged += preference =>
{
// Make sure they have enough loadout points
var wasSelected = Profile?.LoadoutPreferences
.FirstOrDefault(it => it.LoadoutName == selector.Loadout.ID)
?.Selected ?? false;
var selected = preference.Selected && (wasSelected || CheckPoints(-selector.Loadout.Cost, true));
// Update Preferences
Profile = Profile?.WithLoadoutPreference(
selector.Loadout.ID,
selected,
preference.CustomName,
preference.CustomDescription,
preference.CustomContent, // WD EDIT
preference.CustomColorTint,
preference.CustomHeirloom);
IsDirty = true;
UpdateLoadoutPreferences();
SetProfile(Profile, CharacterSlot);
};
}
bool CheckPoints(int points, bool preference)
{
var temp = LoadoutPointsBar.Value + points;
return preference ? temp >= 0 : temp < 0;
}
}
#endregion
#region Functions
private Dictionary<string, object> CreateTree(List<LoadoutCategoryPrototype> cats)
{
var tree = new Dictionary<string, object>();
foreach (var category in cats)
{
// If the category is already in the tree, ignore it
if (tree.ContainsKey(category.ID))
continue;
// Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
// If there are no subcategories, add a loadout spot to the dictionary
if (subCategories.Count == 0)
{
tree.Add(category.ID, new List<LoadoutPrototype>());
continue;
}
// If there are subcategories, we need to add them to the dictionary as well
var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
tree.Add(category.ID, subCategoryTree);
}
return tree;
}
private BoxContainer? FindCategory(string id, NeoTabContainer parent)
{
BoxContainer? match = null;
if(parent.TryFindTabByAlias(id, out var tabId))
match = parent.GetControl<BoxContainer>(tabId);
if (match != null)
return match;
foreach (var subcategory in parent.GetControls<NeoTabContainer>())
match ??= FindCategory(id, subcategory);
return match;
}
private void HideEmptyTabs(List<LoadoutCategoryPrototype> cats)
{
// TODO: HIDE LOGIC LATER
}
private void TryRemoveUnusableLoadouts()
{
// Confirm the user wants to remove unusable loadouts
if (!AdminUIHelpers.TryConfirm(LoadoutsRemoveUnusableButton, _confirmationData))
return;
// Remove unusable and unwearable loadouts
foreach (var (loadout, _) in
_loadouts.Where(l =>
!l.Value || !_loadoutPreferences.First(lps => lps.Loadout.ID == l.Key.ID).Wearable).ToList())
Profile = Profile?.WithLoadoutPreference(loadout.ID, false);
UpdateCharacterRequired();
}
#endregion
#endregion
private void UpdateCharacterRequired()
{
@@ -2808,7 +2419,6 @@ namespace Content.Client.Lobby.UI
RefreshLifepaths();
RefreshJobs();
UpdateTraits(TraitsShowUnusableButton.Pressed);
UpdateLoadouts(LoadoutsShowUnusableButton.Pressed);
}
}
}

View File

@@ -39,8 +39,8 @@ public sealed partial class LoadoutPreferenceSelector : Control
public LoadoutPrototype Loadout { get; }
private LoadoutPreference _preference = null!;
public LoadoutPreference Preference
private Loadout _preference = null!;
public Loadout Preference
{
get => _preference;
set
@@ -53,7 +53,6 @@ public sealed partial class LoadoutPreferenceSelector : Control
if (value.CustomColorTint != null)
UpdatePaint(new(DummyEntityUid, _entityManager.GetComponent<PaintedComponent>(DummyEntityUid)), _entityManager);
HeirloomButton.Pressed = value.CustomHeirloom ?? false;
PreferenceButton.Pressed = value.Selected;
}
}
@@ -84,8 +83,7 @@ public sealed partial class LoadoutPreferenceSelector : Control
}
}
public event Action<LoadoutPreference>? PreferenceChanged;
public event Action<Loadout>? PreferenceChanged;
public LoadoutPreferenceSelector(LoadoutPrototype loadout, JobPrototype highJob,
HumanoidCharacterProfile profile, ref Dictionary<string, EntityUid> entities,
@@ -207,28 +205,6 @@ public sealed partial class LoadoutPreferenceSelector : Control
},
},
});
PreferenceButton.OnToggled += args =>
{
if (args.Pressed == _preference.Selected)
return;
_preference.Selected = args.Pressed;
PreferenceChanged?.Invoke(Preference);
};
HeirloomButton.OnToggled += args =>
{
if (args.Pressed == _preference.Selected)
return;
_preference.CustomHeirloom = args.Pressed ? true : null;
PreferenceChanged?.Invoke(Preference);
};
SaveButton.OnPressed += _ =>
{
_preference.CustomColorTint = SpecialColorTintToggle.Pressed ? ColorEdit.Color.ToHex() : null;
_preference.Selected = PreferenceButton.Pressed;
PreferenceChanged?.Invoke(Preference);
};
// Update prefs cache when something changes
NameEdit.OnTextChanged += _ =>

View File

@@ -104,16 +104,10 @@
</Control>
</BoxContainer>
<!-- Right Panel -->
<PanelContainer Name="RightSide" Access="Public" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True"
<PanelContainer Name="RightSide" Access="Public" StyleClasses="ChatMainPanel" HorizontalAlignment="Right" VerticalExpand="True"
VerticalAlignment="Stretch">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<!-- Top row -->
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public"
SeparationOverride="4">
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center"
HorizontalExpand="True" HorizontalAlignment="Left" />
</BoxContainer>
<controls:HSpacer Spacing="10" />
<Control Margin="0,3,0,0">
<PanelContainer StyleClasses="PaperBackground" Margin="5,10,5,5">
<BoxContainer Orientation="Horizontal">

View File

@@ -0,0 +1,49 @@
using Content.Shared.Clothing.Loadouts.Systems;
namespace Content.Client.Lobby.UI;
// WWDP PARTIAL CLASS
public sealed partial class HumanoidProfileEditor
{
private void InitializeCharacterMenu()
{
Loadouts.OnLoadoutsChanged += OnLoadoutsChange;
}
private void UpdateLoadouts()
{
if (Profile == null)
return;
var highJob = _controller.GetPreferredJob(Profile);
Loadouts.SetData(
Profile.LoadoutPreferencesList,
new(
highJob,
Profile,
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted()
)
);
}
private void CheckpointLoadouts()
{
if (Profile == null)
return;
Loadouts.SetCheckpoint();
}
private void OnLoadoutsChange(List<Loadout> loadouts)
{
if (Profile is null)
return;
Profile = Profile.WithLoadoutPreference(loadouts);
ReloadProfilePreview();
ReloadClothes();
UpdateLoadouts();
}
}

View File

@@ -1,14 +1,13 @@
using Content.StyleSheetify.Client.StyleSheet;
using Content.StyleSheetify.Client.StyleSheet.StyleBox;
using Robust.Client.UserInterface;
namespace Content.Client.Stylesheets;
// WWDP CLASS
public sealed class DummyStylesheetManager : IStylesheetManager
{
public Stylesheet SheetNano { get; } = new Stylesheet([]);
public Stylesheet SheetSpace { get; } = new Stylesheet([]);
public StylesheetReference SheetNano { get; } = StylesheetReference.Empty;
public StylesheetReference SheetSpace { get; } = StylesheetReference.Empty;
public DummyStylesheetManager()
{
@@ -16,5 +15,4 @@ public sealed class DummyStylesheetManager : IStylesheetManager
}
public void Initialize(){}
}

View File

@@ -1,11 +1,12 @@
using Content.StyleSheetify.Client.StyleSheet;
using Robust.Client.UserInterface;
namespace Content.Client.Stylesheets
{
public interface IStylesheetManager
{
Stylesheet SheetNano { get; }
Stylesheet SheetSpace { get; }
StylesheetReference SheetNano { get; } // WWDP EDIT
StylesheetReference SheetSpace { get; } // WWDP EDIT
void Initialize();
}

View File

@@ -1,10 +1,7 @@
using System.Linq;
using Content.StyleSheetify.Client.StyleSheet;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Client.Stylesheets
{
@@ -12,74 +9,17 @@ namespace Content.Client.Stylesheets
{
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IContentStyleSheetManager _contentStyleSheetManager = default!;
[Dependency] private readonly IContentStyleSheetManager _contentStyleSheetManager = default!; // WWDP EDIT
private ISawmill _sawmill = default!;
public StylesheetReference SheetNano { get; private set; } = default!; // WWDP EDIT
public StylesheetReference SheetSpace { get; private set; } = default!; // WWDP EDIT
public Stylesheet SheetNano { get; private set; } = default!;
public Stylesheet SheetSpace { get; private set; } = default!;
// WWDP EDIT START
public void Initialize()
{
_sawmill = Logger.GetSawmill("StylesheetManager");
LoadStyles();
_prototypeManager.PrototypesReloaded += OnPrototypesReloaded;
}
private void LoadStyles()
{
SheetNano = TryMerge(new StyleNano(_resourceCache).Stylesheet, "nano");
SheetSpace = TryMerge(new StyleSpace(_resourceCache).Stylesheet, "space");
SheetNano = _contentStyleSheetManager.MergeStyles(new StyleNano(_resourceCache).Stylesheet, "nano"); // WWDP EDIT
SheetSpace = _contentStyleSheetManager.MergeStyles(new StyleSpace(_resourceCache).Stylesheet, "space"); // WWDP EDIT
_userInterfaceManager.Stylesheet = SheetNano;
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
Logger.Debug("Reloading styles...");
LoadStyles();
}
private Stylesheet TryMerge(Stylesheet stylesheet, string prefix)
{
if (!_prototypeManager.TryIndex<StyleSheetPrototype>(prefix, out var proto))
return stylesheet;
var rules = stylesheet.Rules.ToDictionary(r => r.Selector, r => r);
var newRules = _contentStyleSheetManager.GetStyleRules(proto).ToDictionary(r => r.Selector, r => r);
var mergedPropsCount = 0;
var mergedStylesCount = 0;
var addedStylesCount = 0;
foreach (var (key,value) in newRules)
{
if (rules.TryGetValue(key, out var oriValue))
{
var oriProps = oriValue.Properties.ToDictionary(a => a.Name);
foreach (var props in value.Properties)
{
oriProps[props.Name] = props;
mergedPropsCount++;
}
rules[key] = new(key, oriProps.Values.ToList());
mergedStylesCount++;
}
else
{
rules[key] = value;
addedStylesCount++;
}
}
_sawmill.Debug($"Successfully merged style {prefix}: {mergedPropsCount} props merged and {mergedStylesCount} styles merged and {addedStylesCount} styles added!");
return new(rules.Values.ToList());
}
// WWDP EDIT END
}
}

View File

@@ -6,6 +6,7 @@
xmlns:xe="clr-namespace:Content.Client.UserInterface.XamlExtensions"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.MenuBar.Widgets"
xmlns:controls="clr-namespace:Content.Client._White.UserInterface.Controls"
Name = "MenuButtons"
VerticalExpand="False"
Orientation="Horizontal"
@@ -13,7 +14,11 @@
VerticalAlignment="Top"
SeparationOverride="5"
>
<ui:MenuButton
<controls:FlexBox
HorizontalAlignment="Stretch"
HorizontalExpand="True"
JustifyContent="Center">
<ui:MenuButton
Name="EscapeButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/menu.png'}"
@@ -24,103 +29,104 @@
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"
IconScale="2,2"
/> <!-- WD EDIT -->
<ui:MenuButton
Name="GuidebookButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/chest.png'}"
ToolTip="{Loc 'game-hud-open-guide-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenGuidebook}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="CharacterButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/person.png'}"
ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="EmotesButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/emotion.png'}"
ToolTip="{Loc 'game-hud-open-emotions-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/> <!-- WD EDIT -->
<ui:MenuButton
Name="CraftingButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/craft.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="ActionButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/fist.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="LanguageButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/language.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenLanguageMenu}"
ToolTip="Open the Language Menu"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="AdminButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/panel.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="SandboxButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/sandbox.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="AHelpButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/warning.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
ToolTip="{Loc 'ui-options-function-open-a-help'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
IconScale="2,2"
/>
<ui:MenuButton
Name="GuidebookButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/chest.png'}"
ToolTip="{Loc 'game-hud-open-guide-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenGuidebook}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="CharacterButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/person.png'}"
ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="EmotesButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/emotion.png'}"
ToolTip="{Loc 'game-hud-open-emotions-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/> <!-- WD EDIT -->
<ui:MenuButton
Name="CraftingButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/craft.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="ActionButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/fist.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="LanguageButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/language.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenLanguageMenu}"
ToolTip="Open the Language Menu"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="AdminButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/panel.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="SandboxButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/sandbox.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
IconScale="2,2"
/>
<ui:MenuButton
Name="AHelpButton"
Access="Internal"
Icon="{xe:Tex '/Textures/_White/NovaUI/Icons/warning.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
ToolTip="{Loc 'ui-options-function-open-a-help'}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
IconScale="2,2"
/>
</controls:FlexBox>
</widgets:GameTopMenuBar>

View File

@@ -0,0 +1,99 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared.ContentPack;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client._White.DatumContainer;
public sealed class LocalDatumContainer<T> where T : notnull
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
private readonly ResPath _datumPath;
private readonly ResPath _rootPath = new ResPath("/Datum/");
private Dictionary<string, T> _data = new();
public LocalDatumContainer(string datumName)
{
IoCManager.InjectDependencies(this);
_datumPath = _rootPath / new ResPath($"{datumName}.yaml");
LoadDataFromUserData();
}
public bool TryGetValue(string key,[NotNullWhen(true)] out T? value) =>
_data.TryGetValue(key, out value);
public void SetValue(string key, T? value)
{
if (value is null)
{
RemoveValue(key);
return;
}
_data[key] = value;
Dirty();
}
public void RemoveValue(string key)
{
_data.Remove(key);
Dirty();
}
private void Dirty()
{
try
{
var rootNode = _serializationManager.WriteValue(_data, notNullableOverride: true);
using var stream = _resourceManager.UserData.Open(_datumPath, FileMode.Create);
using var textWriter = new StreamWriter(stream);
rootNode.Write(textWriter);
}
catch (Exception ex)
{
Logger.ErrorS("datum", $"Failed to save datum to {_datumPath}: {ex.Message}");
}
}
private void LoadDataFromUserData()
{
if(!_resourceManager.UserData.IsDir(_rootPath))
_resourceManager.UserData.CreateDir(_rootPath);
if (!_resourceManager.UserData.Exists(_datumPath))
return;
try
{
using var stream = _resourceManager.UserData.Open(_datumPath, FileMode.Open);
using var textReadStream = new StreamReader(stream);
var yamlStream = new YamlStream();
yamlStream.Load(textReadStream);
if (yamlStream.Documents.Count == 0)
return;
var loaded = _serializationManager.Read(
typeof(Dictionary<string, T>),
yamlStream.Documents[0].RootNode.ToDataNode(),
notNullableOverride: false);
if (loaded is Dictionary<string, T> dictionary)
_data = dictionary;
}
catch (Exception ex)
{
Logger.WarningS("datum", $"Failed to load datum from {_datumPath}: {ex.Message}");
}
}
public T? GetValueOrDefault(string key) => _data.GetValueOrDefault(key);
}

View File

@@ -0,0 +1,110 @@
using System.Numerics;
using Content.Shared.Clothing.Loadouts.Prototypes;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._White.Loadouts;
public interface ILoadoutMenuEntry
{
public ILoadoutMenuEntry? Parent { get; set; }
public string Label { get; }
public void Act(BoxContainer loadoutsContainer, LoadoutPicker loadoutPicker);
public void Exit(BoxContainer loadoutsContainer , LoadoutPicker loadoutPicker);
}
public sealed class LoadoutCategoryShowMenuEntry : ILoadoutMenuEntry
{
private readonly ProtoId<LoadoutCategoryPrototype> _loadoutCategory;
public ILoadoutMenuEntry? Parent { get; set; }
public string Label { get; }
public LoadoutCategoryShowMenuEntry(ProtoId<LoadoutCategoryPrototype> loadoutCategory)
{
_loadoutCategory = loadoutCategory;
Label = Loc.GetString($"loadout-category-{loadoutCategory}");
}
public void Act(BoxContainer loadoutsContainer, LoadoutPicker loadoutPicker)
{
loadoutPicker.LoadCategoryButtons(_loadoutCategory);
}
public void Exit(BoxContainer loadoutsContainer, LoadoutPicker loadoutPicker)
{
}
}
public sealed class LoadoutEntriesContainerMenuEntry : ILoadoutMenuEntry
{
public ILoadoutMenuEntry? Parent { get; set;}
public string Label { get; }
private readonly List<ILoadoutMenuEntry> _children = [];
public IReadOnlyList<ILoadoutMenuEntry> Children => _children;
private readonly List<(BaseButton, Action<BaseButton.ButtonEventArgs>)> _currBrns = [];
public LoadoutEntriesContainerMenuEntry(ProtoId<LoadoutCategoryPrototype> loadoutCategoryProtoId)
{
Label = Loc.GetString($"loadout-category-{loadoutCategoryProtoId}");
}
public LoadoutEntriesContainerMenuEntry(string label)
{
Label = label;
}
public void AddChild(params ILoadoutMenuEntry[] children)
{
foreach (var child in children)
{
_children.Add(child);
child.Parent = this;
}
}
public void Act(BoxContainer loadoutsContainer, LoadoutPicker loadoutPicker)
{
foreach (var menuEntry in _children)
{
var button = new Button()
{
Children =
{
new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
SeparationOverride = 15,
Children =
{
new Label()
{
Text = menuEntry.Label,
}
}
}
}
};
Action<BaseButton.ButtonEventArgs> handler = (_) => loadoutPicker.CurrentEntry = menuEntry;
button.OnPressed += handler;
_currBrns.Add((button, handler));
loadoutsContainer.AddChild(button);
}
}
public void Exit(BoxContainer loadoutsContainer, LoadoutPicker loadoutPicker)
{
foreach (var (button, handler) in _currBrns)
{
button.OnPressed -= handler;
}
_currBrns.Clear();
}
}

View File

@@ -0,0 +1,40 @@
<controls:LoadoutEntry
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._White.Loadouts"
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<Control>
<BoxContainer Orientation="Horizontal">
<controls1:StyledButtonGroup Name="ButtonGroup" Orientation="Horizontal" HorizontalExpand="True">
<Button
Name="PreferenceButton"
ToggleMode="True"
VerticalAlignment="Center" >
<BoxContainer Orientation="Horizontal">
<Label Name="LoadoutCostText"
MinWidth="32"
StyleClasses="LabelHeading"
MaxWidth="32"
ClipText="True"
Margin="0,0,8,0"/>
<PanelContainer>
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2f2f2f" />
</PanelContainer.PanelOverride>
<SpriteView Name="PreviewLoadout" OverrideDirection="South" VerticalAlignment="Center" Margin="0" Scale="1.2,1.2" Stretch="Fill"/>
</PanelContainer>
<Label Name="LoadoutNameLabel" Margin="8,0,0,0"/>
</BoxContainer>
</Button>
<Button Visible="False"
Name="HeirloomButton"
Text="{Loc 'humanoid-profile-editor-loadouts-heirloom'}"
ToolTip="{Loc 'humanoid-profile-editor-loadouts-heirloom-tooltip'}"
ToggleMode="True"/>
<Button Name="HeadingButton" Text="{Loc 'humanoid-profile-editor-loadouts-customize'}"/>
</controls1:StyledButtonGroup>
</BoxContainer>
</Control>
</controls:LoadoutEntry>

View File

@@ -0,0 +1,202 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Stylesheets;
using Content.Shared.Clothing.Loadouts.Prototypes;
using Content.Shared.Clothing.Loadouts.Systems;
using Content.Shared.Labels.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._White.Loadouts;
[GenerateTypedNameReferences]
public sealed partial class LoadoutEntry : Control, IComparable<LoadoutEntry>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public Action<LoadoutEntry>? OnLoadoutDirty;
public Action<LoadoutEntry>? OnEditLoadoutRequired;
private Loadout? _currentLoadout;
private EntityUid _loadoutUid;
public Loadout Loadout
{
get => _currentLoadout ?? throw new InvalidOperationException("Loadout not initialized. Call SetLoadout first.");
set => SetLoadout(value);
}
private int _cost;
public int Cost
{
get => _cost;
private set
{
LoadoutCostText.Text = value.ToString();
_cost = value;
}
}
public bool CanWear { get; private set; } = true;
public bool Selected
{
get => PreferenceButton.Pressed;
set => PreferenceButton.Pressed = value;
}
public string LoadoutName
{
get => LoadoutNameLabel.Text ?? string.Empty;
private set => LoadoutNameLabel.Text = value;
}
public string LoadoutDescription { get; private set; } = string.Empty;
private Tooltip _reasonTooltip = new();
public LoadoutEntry()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MouseFilter = MouseFilterMode.Pass;
HeadingButton.OnPressed += HeadingButtonPressed;
PreferenceButton.OnPressed += PreferenceButtonPressed;
TooltipSupplier = OnTooltipSupplierRequired;
}
private Control? OnTooltipSupplierRequired(Control sender) => CanWear ? null : _reasonTooltip;
private void PreferenceButtonPressed(BaseButton.ButtonEventArgs obj)
{
OnLoadoutDirty?.Invoke(this);
}
public bool EnsureIsWearable(CharacterRequirementsArgs args, int loadoutPoint)
{
if (CheckIsWearable(args, loadoutPoint, out var reason))
{
if (CanWear)
return true;
PreferenceButton.RemoveStyleClass(StyleBase.ButtonDanger);
CanWear = true;
PreferenceButton.Disabled = false;
HeirloomButton.Visible = true;
HeadingButton.Visible = true;
PreferenceButton.MouseFilter = MouseFilterMode.Pass;
return true;
}
if (!CanWear)
return false;
PreferenceButton.AddStyleClass(StyleBase.ButtonDanger);
CanWear = false;
PreferenceButton.Disabled = true;
HeirloomButton.Visible = false;
HeadingButton.Visible = false;
PreferenceButton.MouseFilter = MouseFilterMode.Ignore;
_reasonTooltip.SetMessage(FormattedMessage.FromMarkupPermissive(reason));
return false;
}
public bool CheckIsWearable(CharacterRequirementsArgs characterRequirementsArgs, int loadoutPoint,[NotNullWhen(false)] out string? reason)
{
ProtoId<LoadoutPrototype> loadoutPrototype = Loadout.LoadoutName;
reason = null;
if (!_prototypeManager.TryIndex(loadoutPrototype, out var prototype))
{
reason = Loc.GetString("loadout-error-prototype-not-found");
return false;
}
if(prototype.Cost > loadoutPoint)
{
reason = Loc.GetString("loadout-error-too-expensive");
return false;
}
foreach (var requirement in prototype.Requirements)
{
if (!characterRequirementsArgs.IsValid(requirement, prototype, out reason))
return false;
}
return true;
}
private void HeadingButtonPressed(BaseButton.ButtonEventArgs obj)
{
OnEditLoadoutRequired?.Invoke(this);
}
public void SetLoadout(Loadout loadout)
{
_entityManager.DeleteEntity(_loadoutUid);
var loadoutProto = _prototypeManager.Index<LoadoutPrototype>(loadout.LoadoutName);
var firstItem = loadoutProto.Items.FirstOrDefault();
if (firstItem == default)
throw new InvalidOperationException($"Loadout {loadout.LoadoutName} has no items");
_loadoutUid = _entityManager.SpawnEntity(firstItem, MapCoordinates.Nullspace);
Cost = loadoutProto.Cost;
PreviewLoadout.SetEntity(_loadoutUid);
LoadoutName =
Loc.GetString($"loadout-name-{loadoutProto.ID}") == $"loadout-name-{loadoutProto.ID}"
? _entityManager.GetComponent<MetaDataComponent>(_loadoutUid).EntityName
: Loc.GetString($"loadout-name-{loadoutProto.ID}");
// Display the item's label if it's present
if (_entityManager.TryGetComponent(_loadoutUid, out LabelComponent? labelComponent))
{
var itemLabel = labelComponent.CurrentLabel;
if (!string.IsNullOrEmpty(itemLabel))
LoadoutName += $" ({Loc.GetString(itemLabel)})";
}
LoadoutDescription =
!Loc.TryGetString($"loadout-description-{loadoutProto.ID}", out var description)
? _entityManager.GetComponent<MetaDataComponent>(_loadoutUid).EntityDescription
: description;
_currentLoadout = loadout;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
HeadingButton.OnPressed -= HeadingButtonPressed;
PreferenceButton.OnPressed -= PreferenceButtonPressed;
_entityManager.DeleteEntity(_loadoutUid);
}
public int CompareTo(LoadoutEntry? other)
{
if (ReferenceEquals(this, other))
return 0;
if (other is null)
return 1;
var canWearCompare = other.CanWear.CompareTo(CanWear);
if(canWearCompare != 0)
return canWearCompare;
return string.Compare(Loadout.LoadoutName, other.Loadout.LoadoutName, StringComparison.CurrentCulture);
}
}

View File

@@ -0,0 +1,83 @@
using System.Linq;
using Content.Shared.Clothing.Loadouts.Prototypes;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client._White.Loadouts;
public sealed partial class LoadoutPicker
{
private List<LoadoutCategoryPrototype> _rootCategories = [];
public IEnumerable<LoadoutCategoryPrototype> GetRootCategories()
{
return _rootCategories;
}
private ILoadoutMenuEntry? _currentEntry;
public ILoadoutMenuEntry CurrentEntry
{
get => _currentEntry ?? throw new InvalidOperationException();
set
{
_currentEntry?.Exit(Loadouts, this);
ClearupEdit();
ClearLoadoutCategoryButtons();
_currentEntry = value;
EntryBackButton.Visible = _currentEntry.Parent != null;
_currentEntry.Act(Loadouts, this);
}
}
private void CacheRootCategories()
{
_rootCategories =
_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>().Where(p => p.Root)
.ToList();
}
private void InitializeCategories()
{
EntryBackButton.OnPressed += EntryBackButtonPressed;
var rootEntry = new LoadoutEntriesContainerMenuEntry("root");
foreach (var category in GetRootCategories())
{
rootEntry.AddChild(BuildMenuGroup(category.ID).Item1);
}
CurrentEntry = rootEntry;
}
private void EntryBackButtonPressed(BaseButton.ButtonEventArgs obj)
{
if(CurrentEntry.Parent != null)
CurrentEntry = CurrentEntry.Parent;
}
private (ILoadoutMenuEntry, int) BuildMenuGroup(ProtoId<LoadoutCategoryPrototype> categoryPrototypeId)
{
var weight = 0;
if(!_prototypeManager.TryIndex(categoryPrototypeId, out var categoryPrototype))
throw new Exception($"Cannot load prototype {categoryPrototypeId}");
if (categoryPrototype.SubCategories.Count == 0)
return (new LoadoutCategoryShowMenuEntry(categoryPrototypeId), 1);
var entry = new LoadoutEntriesContainerMenuEntry(categoryPrototypeId);
foreach (var category in categoryPrototype.SubCategories)
{
var child = BuildMenuGroup(category);
if(child.Item2 == 0) continue;
entry.AddChild(child.Item1);
weight+= child.Item2;
}
return (entry, weight);
}
}

View File

@@ -0,0 +1,81 @@
<controls:LoadoutPicker
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._White.Loadouts"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls">
<!-- Primary container -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<!-- Loadouts -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer HorizontalExpand="True">
<Button Name="EntryBackButton" Text="..."/>
<Label Name="LoadoutPoints" Text="Дурак!" Margin="8 0" />
<LineEdit Name="LoadoutSearch" PlaceHolder="{Loc 'markings-search'}" HorizontalExpand="True" />
<Button Name="ResetButton" Text="{Loc 'ui-options-bind-reset'}" StyleClasses="OpenLeft" />
</BoxContainer>
<PanelContainer>
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<ScrollContainer HorizontalExpand="True" MinSize="300 300">
<BoxContainer Name="Loadouts" Orientation="Vertical" HorizontalExpand="True" MinSize="300 300" />
</ScrollContainer>
</PanelContainer>
</BoxContainer>
<!-- Loadout config -->
<PanelContainer HorizontalExpand="True" Name="LoadoutConfigContainer" Visible="False">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2f2f2f" BorderColor="#2f2f2faf" BorderThickness="1" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" Margin="3" HorizontalExpand="True">
<BoxContainer Name="SpecialName" Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-loadouts-customize-name'}" />
<LineEdit Name="NameEdit" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Name="SpecialDescription" Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-loadouts-customize-description'}" />
<PanelContainer HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#222222" />
</PanelContainer.PanelOverride>
<controls1:ResizableControl
AllowedResizeDirection="Vertical"
HorizontalExpand="True"
MinSize="128 64">
<TextEdit Name="DescriptionEdit" HorizontalExpand="True" VerticalExpand="True" Margin="3" />
</controls1:ResizableControl>
</PanelContainer>
</BoxContainer>
<!--WD EDIT START-->
<BoxContainer Name="SpecialBookText" Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-loadouts-customize-book-text'}" />
<PanelContainer HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#222222" />
</PanelContainer.PanelOverride>
<controls1:ResizableControl
AllowedResizeDirection="Vertical"
HorizontalExpand="True"
MinSize="128 64">
<TextEdit Name="BookTextEdit" StyleClasses="PaperLineEdit" HorizontalExpand="True" VerticalExpand="True" Margin="3" />
</controls1:ResizableControl>
</PanelContainer>
</BoxContainer>
<!--WD EDIT END-->
<Button Name="SpecialColorTintToggle" Text="{Loc 'humanoid-profile-editor-loadouts-customize-color'}" ToggleMode="True" Margin="0 3 0 0" StyleClasses="OpenBoth" />
<ColorSelectorSliders Name="ColorEdit" Visible="False" Color="#fff" HorizontalExpand="True" />
<Button Name="SaveButton" Text="{Loc 'humanoid-profile-editor-loadouts-customize-save'}" HorizontalExpand="True" Margin="0 3 0 0" StyleClasses="OpenBoth" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</controls:LoadoutPicker>

View File

@@ -0,0 +1,489 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client._White.DatumContainer;
using Content.Shared.CCVar;
using Content.Shared.Clothing.Loadouts.Prototypes;
using Content.Shared.Clothing.Loadouts.Systems;
using Content.Shared.Customization.Systems;
using Content.Shared.Mind;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._White.Loadouts;
[GenerateTypedNameReferences]
public sealed partial class LoadoutPicker : Control
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private readonly Dictionary<string, Loadout> _selectedLoadouts = [];
private readonly List<LoadoutEntry> _loadoutEntries = new();
private readonly LocalDatumContainer<string> _customColorTints;
private readonly LocalDatumContainer<bool> _customHeirloom;
private readonly LocalDatumContainer<string> _customName;
private readonly LocalDatumContainer<string> _customDescription;
private readonly LocalDatumContainer<string> _customContent;
private List<Loadout> _checkpointLoadouts = new();
private CharacterRequirementsArgs? _checkpointRequirements;
public Action<List<Loadout>>? OnLoadoutsChanged;
private int MaxLoadoutPoints => _configManager.GetCVar(CCVars.GameLoadoutsPoints);
private int _loadoutPoints = 0;
public CharacterRequirementsArgs CharacterRequirementsArgs = default!;
private ProtoId<LoadoutCategoryPrototype>? _selectedLoadoutCategory;
private Dictionary<ProtoId<LoadoutCategoryPrototype>, List<LoadoutPrototype>> _loadoutCache = [];
public int LoadoutPoint
{
get => _loadoutPoints;
set
{
LoadoutPoints.Text = Loc.GetString("marking-points-remaining", ("points", value));
_loadoutPoints = value;
}
}
public LoadoutPicker()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_customName = new("custom_name");
_customDescription = new("custom_description");
_customContent = new("custom_content");
_customHeirloom = new("custom_heirloom");
_customColorTints = new("custom_color_tints");
SaveButton.OnPressed += SaveButtonPressed;
SpecialColorTintToggle.OnPressed += SpecialColorTintTogglePressed;
ResetButton.OnPressed += ResetButtonPressed;
LoadoutSearch.OnTextChanged += args => Populate(args.Text);
_prototypeManager.PrototypesReloaded += OnPrototypesReloaded;
InitializeCache();
InitializeCategories();
}
private void InitializeCache()
{
CacheRootCategories();
_loadoutCache.Clear();
foreach (var loadoutPrototype in _prototypeManager.EnumeratePrototypes<LoadoutPrototype>())
{
if (!_loadoutCache.TryGetValue(loadoutPrototype.Category, out var loadoutList))
{
loadoutList = new();
_loadoutCache.Add(loadoutPrototype.Category, loadoutList);
}
loadoutList.Add(loadoutPrototype);
}
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
if (obj.WasModified<LoadoutPrototype>())
InitializeCache();
}
private void Populate(string _)
{
if (_selectedLoadoutCategory != null)
LoadCategoryButtons(_selectedLoadoutCategory.Value);
}
private void ResetButtonPressed(BaseButton.ButtonEventArgs obj)
{
if(_checkpointRequirements is null)
return;
SetData(_checkpointLoadouts, _checkpointRequirements);
}
public void SetCheckpoint()
{
_checkpointLoadouts = _selectedLoadouts.Values.ToList();
_checkpointRequirements = CharacterRequirementsArgs;
}
public bool IsCategoryValid(ProtoId<LoadoutCategoryPrototype> category) => _loadoutCache.ContainsKey(category);
public void SetData(IEnumerable<Loadout> selectedPreferenceList, CharacterRequirementsArgs characterRequirements)
{
ClearLoadouts();
ClearupEdit();
LoadoutSearch.Clear();
CharacterRequirementsArgs = characterRequirements;
foreach (var preference in selectedPreferenceList)
{
if (!_prototypeManager.TryIndex<LoadoutPrototype>(preference.LoadoutName, out var proto))
{
Logger.Error($"Cannot add loadout to selected loadouts: prototype {preference.LoadoutName} not found");
continue;
}
var loadoutEntry = CreateEntry(preference.LoadoutName);
if (!TrySelectLoadout(loadoutEntry))
{
Logger.Warning($"Removing loadout {preference.LoadoutName} from selected list.");
continue;
}
LoadoutPoint -= loadoutEntry.Cost;
if (proto.CustomContent && loadoutEntry.Loadout.CustomContent != preference.CustomContent)
{
Logger.Warning("CustomContent mismatch, syncing...");
loadoutEntry.Loadout.CustomContent = preference.CustomContent;
_customContent.SetValue(preference.LoadoutName, preference.CustomContent);
}
if (proto.CustomName && loadoutEntry.Loadout.CustomName != preference.CustomName)
{
Logger.Warning("CustomName mismatch, syncing...");
loadoutEntry.Loadout.CustomName = preference.CustomName;
_customName.SetValue(preference.LoadoutName, preference.CustomName);
}
if (proto.CustomDescription && loadoutEntry.Loadout.CustomDescription != preference.CustomDescription)
{
Logger.Warning("CustomDescription mismatch, syncing...");
loadoutEntry.Loadout.CustomDescription = preference.CustomDescription;
_customDescription.SetValue(preference.LoadoutName, preference.CustomDescription);
}
if (proto.CanBeHeirloom && loadoutEntry.Loadout.CustomHeirloom != preference.CustomHeirloom)
{
Logger.Warning("CustomHeirloom mismatch, syncing...");
loadoutEntry.Loadout.CustomHeirloom = preference.CustomHeirloom;
if( preference.CustomHeirloom is null)
_customHeirloom.RemoveValue(preference.LoadoutName);
else
_customHeirloom.SetValue(preference.LoadoutName, preference.CustomHeirloom.Value);
}
if (proto.CustomColorTint && loadoutEntry.Loadout.CustomColorTint != preference.CustomColorTint)
{
Logger.Warning("CustomColorTint mismatch, syncing...");
loadoutEntry.Loadout.CustomColorTint = preference.CustomColorTint;
_customColorTints.SetValue(preference.LoadoutName, preference.CustomColorTint);
}
}
CurrentEntry = CurrentEntry;
}
public bool LoadCategoryButtons(ProtoId<LoadoutCategoryPrototype> loadoutCategoryPrototype)
{
ClearLoadoutCategoryButtons();
ClearupEdit();
var loadoutPrototypes = GroupLoadoutsByGroup(loadoutCategoryPrototype).ToList();
if (loadoutPrototypes.Count == 0)
return false;
foreach (var loadoutPrototype in loadoutPrototypes)
{
var loadoutEntry = new LoadoutEntry();
if(_selectedLoadouts.TryGetValue(loadoutPrototype.ID, out var loadout))
{
loadoutEntry.SetLoadout(loadout);
loadoutEntry.Selected = true;
}
else
{
var newLoadout = new Loadout(
loadoutPrototype.ID,
loadoutPrototype.CustomName ? _customName.GetValueOrDefault(loadoutPrototype.ID) : null,
loadoutPrototype.CustomDescription ? _customDescription.GetValueOrDefault(loadoutPrototype.ID) : null,
loadoutPrototype.CustomContent ? _customContent.GetValueOrDefault(loadoutPrototype.ID) : null,
loadoutPrototype.CustomColorTint ? _customColorTints.GetValueOrDefault(loadoutPrototype.ID) : null,
loadoutPrototype.CanBeHeirloom ? _customHeirloom.GetValueOrDefault(loadoutPrototype.ID) : null
);
loadoutEntry.SetLoadout(newLoadout);
}
if (!string.IsNullOrEmpty(LoadoutSearch.Text) && !loadoutEntry.LoadoutName.Contains(LoadoutSearch.Text))
continue;
loadoutEntry.OnEditLoadoutRequired += OnEntryEditLoadoutRequired;
loadoutEntry.OnLoadoutDirty += OnEntryLoadoutDirty;
loadoutEntry.EnsureIsWearable(CharacterRequirementsArgs, LoadoutPoint);
_loadoutEntries.Add(loadoutEntry);
}
SortAndPasteEntries();
_selectedLoadoutCategory = loadoutCategoryPrototype;
return true;
}
private void Dirty()
{
OnLoadoutsChanged?.Invoke(_selectedLoadouts.Select(x => x.Value).ToList());
}
private LoadoutEntry CreateEntry(string loadoutName)
{
if (!_prototypeManager.TryIndex<LoadoutPrototype>(loadoutName, out var prototype))
{
throw new Exception("Could not find a prototype " + loadoutName);
}
var loadout = new Loadout(
loadoutName,
prototype.CustomName ? _customName.GetValueOrDefault(loadoutName) : null,
prototype.CustomDescription ? _customDescription.GetValueOrDefault(loadoutName) : null,
prototype.CustomContent ? _customContent.GetValueOrDefault(loadoutName) : null,
prototype.CustomColorTint ? _customColorTints.GetValueOrDefault(loadoutName) : null,
prototype.CanBeHeirloom ? _customHeirloom.GetValueOrDefault(loadoutName) : null
);
var entry = new LoadoutEntry();
entry.SetLoadout(loadout);
return entry;
}
private void SpecialColorTintTogglePressed(BaseButton.ButtonEventArgs obj)
{
if(_currPrototype == null || !_currPrototype.CustomColorTint)
return;
ColorEdit.Visible = SpecialColorTintToggle.Pressed;
}
private void OnEntryLoadoutDirty(LoadoutEntry entry)
{
if (TryFreeLoadout(entry.Loadout))
{
entry.Selected = false;
Dirty();
return;
}
entry.Selected = TrySelectLoadout(entry);
Dirty();
}
private bool TrySelectLoadout(LoadoutEntry entry)
{
if (entry.EnsureIsWearable(CharacterRequirementsArgs, LoadoutPoint))
{
_selectedLoadouts.Add(entry.Loadout.LoadoutName, entry.Loadout);
return true;
}
return false;
}
private bool TryFreeLoadout(Loadout loadout)
{
if (!_selectedLoadouts.Remove(loadout.LoadoutName, out _))
return false;
if(_prototypeManager.TryIndex<LoadoutPrototype>(loadout.LoadoutName, out var prototype))
LoadoutPoint += prototype.Cost;
return true;
}
private void OnEntryEditLoadoutRequired(LoadoutEntry entry)
{
EditLoadout(entry);
}
private LoadoutPrototype? _currPrototype;
private LoadoutEntry? _currEdit;
public void EditLoadout(LoadoutEntry loadoutEntry)
{
ClearupEdit();
var loadout = loadoutEntry.Loadout;
if (!_prototypeManager.TryIndex<LoadoutPrototype>(loadout.LoadoutName, out var loadoutPrototype))
{
Logger.Error($"Unable to load loadout: unknown prototype {loadout.LoadoutName}");
return;
}
LoadoutConfigContainer.Visible = true;
SpecialName.Visible = loadoutPrototype.CustomName;
SpecialDescription.Visible = loadoutPrototype.CustomDescription;
SpecialBookText.Visible = loadoutPrototype.CustomContent;
SpecialColorTintToggle.Visible = loadoutPrototype.CustomColorTint;
if (loadoutPrototype.CustomName)
NameEdit.Text = loadout.CustomName ?? "";
if (loadoutPrototype.CustomDescription)
DescriptionEdit.TextRope = new Rope.Leaf(loadout.CustomDescription ?? "");
if(loadoutPrototype.CustomContent)
BookTextEdit.TextRope = new Rope.Leaf(loadout.CustomContent ?? "");
if (loadoutPrototype.CustomColorTint )
{
if(loadout.CustomColorTint is not null)
{
SpecialColorTintToggle.Pressed = true;
ColorEdit.Color = Color.FromHex(loadout.CustomColorTint);
ColorEdit.Visible = true;
}
else
{
SpecialColorTintToggle.Pressed = false;
ColorEdit.Visible = false;
}
}
_currEdit = loadoutEntry;
_currPrototype = loadoutPrototype;
}
private void ClearLoadoutCategoryButtons()
{
foreach (var entry in _loadoutEntries)
{
entry.OnEditLoadoutRequired -= OnEntryEditLoadoutRequired;
entry.OnLoadoutDirty -= OnEntryLoadoutDirty;
}
_loadoutEntries.Clear();
Loadouts.Children.Clear();
}
private void SortAndPasteEntries()
{
Loadouts.Children.Clear();
_loadoutEntries.Sort();
foreach (var entry in _loadoutEntries)
{
Loadouts.Children.Add(entry);
}
}
private void SaveButtonPressed(BaseButton.ButtonEventArgs obj)
{
if(_currEdit == null)
return;
var oldValue = _currEdit.Loadout;
oldValue.CustomName = TextOrNull(NameEdit.Text);
oldValue.CustomContent = TextOrNull(BookTextEdit.TextRope);
oldValue.CustomDescription = TextOrNull(DescriptionEdit.TextRope);
oldValue.CustomColorTint = _currPrototype?.CustomColorTint == true && SpecialColorTintToggle.Pressed
? ColorEdit.Color.ToHex()
: null;
if(oldValue.CustomName != null) _customName.SetValue(oldValue.LoadoutName, oldValue.CustomName);
else _customName.RemoveValue(oldValue.LoadoutName);
if(oldValue.CustomContent != null) _customContent.SetValue(oldValue.LoadoutName, oldValue.CustomContent);
else _customContent.RemoveValue(oldValue.LoadoutName);
if(oldValue.CustomDescription != null) _customDescription.SetValue(oldValue.LoadoutName, oldValue.CustomDescription);
else _customDescription.RemoveValue(oldValue.LoadoutName);
if(oldValue.CustomColorTint != null) _customColorTints.SetValue(oldValue.LoadoutName, oldValue.CustomColorTint);
else _customColorTints.RemoveValue(oldValue.LoadoutName);
ClearupEdit();
Dirty();
}
private void ClearupEdit()
{
LoadoutConfigContainer.Visible = false;
_currEdit = null;
_currPrototype = null;
NameEdit.Text = "";
DescriptionEdit.TextRope = Rope.Leaf.Empty;
BookTextEdit.TextRope = Rope.Leaf.Empty;
SpecialColorTintToggle.Pressed = false;
ColorEdit.Visible = false;
}
private string? TextOrNull(Rope.Node node)
{
var text = Rope.Collapse(node);
return string.IsNullOrEmpty(text) ? null : text;
}
private string? TextOrNull(string text)
{
if (string.IsNullOrEmpty(text))
return null;
return text;
}
private void ClearLoadouts()
{
ClearLoadoutCategoryButtons();
_selectedLoadouts.Clear();
LoadoutPoint = MaxLoadoutPoints;
}
private IEnumerable<LoadoutPrototype> GroupLoadoutsByGroup(ProtoId<LoadoutCategoryPrototype> loadoutCategoryPrototype)
{
if (_loadoutCache.TryGetValue(loadoutCategoryPrototype, out var loadouts))
return loadouts;
return [];
}
}
public sealed class CharacterRequirementsArgs(
JobPrototype job,
HumanoidCharacterProfile profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
bool whitelisted,
int depth = 0,
MindComponent? mind = null,
IDependencyCollection? dependencies = null)
{
public JobPrototype Job { get; set; } = job;
public HumanoidCharacterProfile Profile { get; set; } = profile;
public IReadOnlyDictionary<string, TimeSpan> PlayTimes { get; set; } = playTimes;
public bool Whitelisted { get; set; } = whitelisted;
public int Depth { get; set; } = depth;
public MindComponent? Mind { get; set; } = mind;
public IDependencyCollection? Dependencies { get; set; } = dependencies;
public bool IsValid(CharacterRequirement requirement, IPrototype prototype,[NotNullWhen(false)] out string? reason)
{
Dependencies ??= IoCManager.Instance!;
var valid = requirement.IsValid(
Job,
Profile,
PlayTimes,
Whitelisted,
prototype,
Dependencies.Resolve<IEntityManager>(),
Dependencies.Resolve<IPrototypeManager>(),
Dependencies.Resolve<IConfigurationManager>(),
out reason,
Depth,
Mind);
return requirement.Inverted ? !valid : valid;
}
}

View File

@@ -0,0 +1,345 @@
using System.Linq;
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client._White.UserInterface.Controls;
[Virtual]
public class FlexBox : Container
{
public enum FlexAlignContent
{
FlexStart,
FlexEnd,
Center,
Stretch,
SpaceBetween,
SpaceAround
}
public enum FlexAlignItems
{
FlexStart,
FlexEnd,
Center,
Stretch
}
public enum FlexDirection
{
Row,
RowReverse,
Column,
ColumnReverse
}
public enum FlexJustifyContent
{
FlexStart,
FlexEnd,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly
}
public enum FlexWrap
{
NoWrap,
Wrap,
WrapReverse
}
public const string StylePropertyGap = "gap";
public const string StylePropertyRowGap = "row-gap";
public const string StylePropertyColumnGap = "column-gap";
public const string StylePropertyAlignItems = "align-items";
public const string StylePropertyOrder = "order";
private const float DefaultGap = 0f;
public FlexDirection Direction { get; set; } = FlexDirection.Row;
public FlexWrap Wrap { get; set; } = FlexWrap.Wrap;
public FlexAlignItems AlignItems { get; set; } = FlexAlignItems.Stretch;
public FlexJustifyContent JustifyContent { get; set; } = FlexJustifyContent.FlexStart;
public FlexAlignContent AlignContent { get; set; } = FlexAlignContent.FlexStart;
public float? GapOverride { get; set; }
public float? RowGapOverride { get; set; }
public float? ColumnGapOverride { get; set; }
private float ActualGap => GetStyleFloat(StylePropertyGap, GapOverride, DefaultGap);
private float ActualRowGap => GetStyleFloat(StylePropertyRowGap, RowGapOverride, ActualGap);
private float ActualColumnGap => GetStyleFloat(StylePropertyColumnGap, ColumnGapOverride, ActualGap);
private float GetStyleFloat(string property, float? overrideValue, float defaultValue)
{
if (overrideValue.HasValue)
return overrideValue.Value;
if (TryGetStyleProperty(property, out float value))
return value;
return defaultValue;
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var children = new List<Control>(Children.Count());
foreach (var child in Children)
{
if (child.Visible)
children.Add(child);
}
if (children.Count == 0)
return Vector2.Zero;
foreach (var child in children)
child.Measure(Vector2.PositiveInfinity);
var lines = BuildLines(children, availableSize);
var isRow = IsRow;
var main = lines.Max(l => l.MainSize);
var cross = lines.Sum(l => l.FinalCrossSize) +
Math.Max(0, lines.Count - 1) * CrossGap;
return isRow
? new Vector2(main, cross)
: new Vector2(cross, main);
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var children = Children.Where(c => c.Visible).ToList();
if (children.Count == 0)
return finalSize;
var lines = BuildLines(children, finalSize);
PositionLines(lines, finalSize);
foreach (var line in lines)
PositionItems(line, finalSize);
return finalSize;
}
private bool IsRow =>
Direction == FlexDirection.Row ||
Direction == FlexDirection.RowReverse;
private bool IsReverse =>
Direction == FlexDirection.RowReverse ||
Direction == FlexDirection.ColumnReverse;
private float MainGap => IsRow ? ActualColumnGap : ActualRowGap;
private float CrossGap => IsRow ? ActualRowGap : ActualColumnGap;
private List<FlexLine> BuildLines(
List<Control> children,
Vector2 size)
{
children.Sort(static (a, b) =>
{
var ao = a.TryGetStyleProperty(StylePropertyOrder, out int av) ? av : 0;
var bo = b.TryGetStyleProperty(StylePropertyOrder, out int bv) ? bv : 0;
return ao.CompareTo(bo);
});
var lines = new List<FlexLine>();
var line = new FlexLine();
var maxMain = IsRow ? size.X : size.Y;
var wrap = Wrap != FlexWrap.NoWrap && !float.IsInfinity(maxMain);
foreach (var child in children)
{
var main = IsRow ? child.DesiredSize.X : child.DesiredSize.Y;
var cross = IsRow ? child.DesiredSize.Y : child.DesiredSize.X;
var projected =
line.Items.Count == 0
? main
: line.MainSize + MainGap + main;
if (wrap && projected > maxMain && line.Items.Count > 0)
{
lines.Add(line);
line = new FlexLine();
}
line.Add(child, main, cross, MainGap);
}
if (line.Items.Count > 0)
lines.Add(line);
return lines;
}
private void PositionLines(List<FlexLine> lines, Vector2 size)
{
var crossSize = IsRow ? size.Y : size.X;
var totalCross =
lines.Sum(l => l.FinalCrossSize) +
Math.Max(0, lines.Count - 1) * CrossGap;
var free = Math.Max(0, crossSize - totalCross);
float offset = 0;
float extraGap = 0;
foreach (var l in lines)
l.ExtraCrossSize = 0;
switch (AlignContent)
{
case FlexAlignContent.FlexEnd:
offset = free;
break;
case FlexAlignContent.Center:
offset = free / 2;
break;
case FlexAlignContent.SpaceBetween:
if (lines.Count > 1)
extraGap = free / (lines.Count - 1);
break;
case FlexAlignContent.SpaceAround:
extraGap = free / lines.Count;
offset = extraGap / 2;
break;
case FlexAlignContent.Stretch:
var add = free / lines.Count;
foreach (var l in lines)
l.ExtraCrossSize = add;
break;
}
var pos = offset;
var wrapReverse = Wrap == FlexWrap.WrapReverse;
foreach (var line in lines)
{
line.CrossPos = wrapReverse
? crossSize - pos - line.FinalCrossSize
: pos;
pos += line.FinalCrossSize + CrossGap + extraGap;
}
}
private void PositionItems(FlexLine line, Vector2 size)
{
var mainSize = IsRow ? size.X : size.Y;
var free = Math.Max(0, mainSize - line.MainSize);
float offset = 0;
float extraGap = 0;
switch (JustifyContent)
{
case FlexJustifyContent.FlexEnd:
offset = free;
break;
case FlexJustifyContent.Center:
offset = free / 2;
break;
case FlexJustifyContent.SpaceBetween:
if (line.Items.Count > 1)
extraGap = free / (line.Items.Count - 1);
break;
case FlexJustifyContent.SpaceAround:
extraGap = free / line.Items.Count;
offset = extraGap / 2;
break;
case FlexJustifyContent.SpaceEvenly:
extraGap = free / (line.Items.Count + 1);
offset = extraGap;
break;
}
var pos = offset;
if (IsReverse)
{
for (var i = line.Items.Count - 1; i >= 0; i--)
PositionItem(line.Items[i]);
}
else
{
for (var i = 0; i < line.Items.Count; i++)
PositionItem(line.Items[i]);
}
return;
void PositionItem(FlexItem item)
{
var align = GetEffectiveAlignItems(item.Control);
var crossSize = align == FlexAlignItems.Stretch
? line.FinalCrossSize
: item.CrossSize;
var crossPos = align switch
{
FlexAlignItems.Center => line.CrossPos + (line.FinalCrossSize - crossSize) / 2,
FlexAlignItems.FlexEnd => line.CrossPos + line.FinalCrossSize - crossSize,
_ => line.CrossPos
};
var rect = IsRow
? new UIBox2(pos, crossPos, pos + item.MainSize, crossPos + crossSize)
: new UIBox2(crossPos, pos, crossPos + crossSize, pos + item.MainSize);
item.Control.Arrange(rect);
pos += item.MainSize + MainGap + extraGap;
}
}
private int GetFlexOrder(Control c) =>
c.TryGetStyleProperty(StylePropertyOrder, out int o) ? o : 0;
private FlexAlignItems GetEffectiveAlignItems(Control c) =>
c.TryGetStyleProperty(StylePropertyAlignItems, out FlexAlignItems a)
? a
: AlignItems;
private sealed class FlexLine
{
public readonly List<FlexItem> Items = new();
public float MainSize;
public float CrossSize;
public float ExtraCrossSize;
public float CrossPos;
public float FinalCrossSize => CrossSize + ExtraCrossSize;
public void Add(Control c, float main, float cross, float gap)
{
if (Items.Count > 0)
MainSize += gap;
Items.Add(new FlexItem(c, main, cross));
MainSize += main;
CrossSize = Math.Max(CrossSize, cross);
}
}
private sealed class FlexItem
{
public readonly Control Control;
public readonly float MainSize;
public readonly float CrossSize;
public FlexItem(Control c, float main, float cross)
{
Control = c;
MainSize = main;
CrossSize = cross;
}
}
}

View File

@@ -139,7 +139,7 @@ public static partial class PoolManager
typeof(PoolManager).Assembly,
typeof(Content.StyleSheetify.Client.EntryPoint).Assembly, //WWDP EDIT
typeof(Content.StyleSheetify.Shared.StylePrototypeIgnorance).Assembly, //WWDP EDIT
typeof(Content.StyleSheetify.Shared.Dynamic.DynamicValue).Assembly, //WWDP EDIT
}
};
@@ -440,7 +440,7 @@ we are just going to end this here to save a lot of time. This is the exception
typeof(PoolManager).Assembly,
typeof(Content.StyleSheetify.Server.EntryPoint).Assembly, //WWDP EDIT
typeof(Content.StyleSheetify.Shared.StylePrototypeIgnorance).Assembly, //WWDP EDIT
typeof(Content.StyleSheetify.Shared.Dynamic.DynamicValue).Assembly, //WWDP EDIT
];
_contentAssemblies.UnionWith(extraAssemblies);

View File

@@ -30,8 +30,8 @@ public sealed class SandboxTest
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Client.Entry.EntryPoint).Assembly,
typeof(StyleSheetify.Client.EntryPoint).Assembly,
typeof(StyleSheetify.Shared.StylePrototypeIgnorance).Assembly,
typeof(StyleSheetify.Client.EntryPoint).Assembly, // WWDP EDIT
typeof(StyleSheetify.Shared.Dynamic.DynamicValue).Assembly, // WWDP EDIT
},
Options = new GameControllerOptions { LoadConfigAndUserData = false }
};

View File

@@ -69,7 +69,7 @@ namespace Content.Server.Database
.HasIndex(p => new { HumanoidProfileId = p.ProfileId, p.TraitName })
.IsUnique();
modelBuilder.Entity<Loadout>()
modelBuilder.Entity<LoadoutItem>()
.HasIndex(p => new { HumanoidProfileId = p.ProfileId, p.LoadoutName })
.IsUnique();
@@ -427,7 +427,7 @@ namespace Content.Server.Database
public List<Job> Jobs { get; } = new();
public List<Antag> Antags { get; } = new();
public List<Trait> Traits { get; } = new();
public List<Loadout> Loadouts { get; } = new();
public List<LoadoutItem> Loadouts { get; } = new();
[Column("pref_unavailable")] public DbPreferenceUnavailableMode PreferenceUnavailable { get; set; }
@@ -472,21 +472,36 @@ namespace Content.Server.Database
public string TraitName { get; set; } = null!;
}
[Serializable]
public partial class Loadout : Shared.Clothing.Loadouts.Systems.Loadout
[Table("loadout")]
public class LoadoutItem
{
public string LoadoutName { get; set; } = null!;
public string? CustomName { get; set; }
public string? CustomDescription { get; set; }
public string? CustomContent { get; set; } // WD EDIT
public string? CustomColorTint { get; set; }
public bool? CustomHeirloom { get; set; }
public int Id { get; set; }
public Profile Profile { get; set; } = null!;
public int ProfileId { get; set; }
public Loadout(
public LoadoutItem(
string loadoutName,
string? customName = null,
string? customDescription = null,
string? customContent = null, // WD EDIT
string? customColorTint = null,
bool? customHeirloom = null
) : base(loadoutName, customName, customDescription, customContent, customColorTint, customHeirloom) { } // WD EDIT
)
{
LoadoutName = loadoutName;
CustomName = customName;
CustomDescription = customDescription;
CustomContent = customContent;
CustomColorTint = customColorTint;
CustomHeirloom = customHeirloom;
}
}
public enum DbPreferenceUnavailableMode

View File

@@ -201,7 +201,17 @@ namespace Content.Server.Database
var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
var loadouts = profile.Loadouts.Select(Shared.Clothing.Loadouts.Systems.Loadout (l) => l);
// WWDP EDIT START
var loadouts = profile.Loadouts.Select(l =>
new Loadout(
l.LoadoutName,
l.CustomName,
l.CustomDescription,
l.CustomContent,
l.CustomColorTint,
l.CustomHeirloom
));
// WWDP EDIT END
var sex = Sex.Male;
if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
@@ -283,15 +293,7 @@ namespace Content.Server.Database
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToHashSet(),
traits.ToHashSet(),
loadouts.Select(l => new LoadoutPreference(l.LoadoutName)
{
CustomName = l.CustomName,
CustomDescription = l.CustomDescription,
CustomContent = l.CustomContent, // WD EDIT
CustomColorTint = l.CustomColorTint,
CustomHeirloom = l.CustomHeirloom,
Selected = true,
}).ToHashSet()
loadouts.ToDictionary(p => p.LoadoutName) // WWDP EDIT
);
}
@@ -357,8 +359,8 @@ namespace Content.Server.Database
);
profile.Loadouts.Clear();
profile.Loadouts.AddRange(humanoid.LoadoutPreferences
.Select(l => new Loadout(l.LoadoutName, l.CustomName, l.CustomDescription, l.CustomContent, l.CustomColorTint, l.CustomHeirloom))); // WD EDIT
profile.Loadouts.AddRange(humanoid.LoadoutPreferencesList
.Select(l => new LoadoutItem(l.LoadoutName, l.CustomName, l.CustomDescription, l.CustomContent, l.CustomColorTint, l.CustomHeirloom))); // WD EDIT
// WWDP EDIT START
profile.BarkPause = humanoid.BarkSettings.Pause;

View File

@@ -54,13 +54,13 @@ public sealed class SharedLoadoutSystem : EntitySystem
_station.EquipStartingGear(uid, proto);
}
public (List<EntityUid>, List<(EntityUid, LoadoutPreference, int)>) ApplyCharacterLoadout(
public (List<EntityUid>, List<(EntityUid, Loadout, int)>) ApplyCharacterLoadout(
EntityUid uid,
ProtoId<JobPrototype> job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
out List<(EntityUid, LoadoutPreference)> heirlooms)
out List<(EntityUid, Loadout)> heirlooms)
{
var jobPrototype = _prototype.Index(job);
return ApplyCharacterLoadout(uid, jobPrototype, profile, playTimes, whitelisted, out heirlooms);
@@ -76,21 +76,21 @@ public sealed class SharedLoadoutSystem : EntitySystem
/// <param name="whitelisted">If the player is whitelisted</param>
/// <param name="heirlooms">Every entity the player selected as a potential heirloom</param>
/// <returns>A list of loadout items that couldn't be equipped but passed checks</returns>
public (List<EntityUid>, List<(EntityUid, LoadoutPreference, int)>) ApplyCharacterLoadout(
public (List<EntityUid>, List<(EntityUid, Loadout, int)>) ApplyCharacterLoadout(
EntityUid uid,
JobPrototype job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
out List<(EntityUid, LoadoutPreference)> heirlooms)
out List<(EntityUid, Loadout)> heirlooms)
{
var failedLoadouts = new List<EntityUid>();
var allLoadouts = new List<(EntityUid, LoadoutPreference, int)>();
var allLoadouts = new List<(EntityUid, Loadout, int)>();
heirlooms = new();
if (!job.SpawnLoadout)
return (failedLoadouts, allLoadouts);
foreach (var loadout in profile.LoadoutPreferences)
foreach (var loadout in profile.LoadoutPreferencesList) // WWDP EDIT
{
var slot = "";
@@ -192,7 +192,7 @@ public sealed class SharedLoadoutSystem : EntitySystem
}
[Serializable, NetSerializable, ImplicitDataDefinitionForInheritors]
public abstract partial class Loadout
public sealed partial class Loadout
{
[DataField] public string LoadoutName { get; set; }
[DataField] public string? CustomName { get; set; }
@@ -201,7 +201,7 @@ public abstract partial class Loadout
[DataField] public string? CustomColorTint { get; set; }
[DataField] public bool? CustomHeirloom { get; set; }
protected Loadout(
public Loadout(
string loadoutName,
string? customName = null,
string? customDescription = null,
@@ -219,21 +219,6 @@ public abstract partial class Loadout
}
}
[Serializable, NetSerializable]
public sealed partial class LoadoutPreference : Loadout
{
[DataField] public bool Selected;
public LoadoutPreference(
string loadoutName,
string? customName = null,
string? customDescription = null,
string? customContent = null, // WD EDIT
string? customColorTint = null,
bool? customHeirloom = null
) : base(loadoutName, customName, customDescription, customContent, customColorTint, customHeirloom) { } // WD EDIT
}
/// <summary>
/// Event raised both directed and broadcast when a player has been spawned and then given a Loadout.
/// Most useful to delay actions that should happen on spawn for when the loadouts have been handled.

View File

@@ -373,7 +373,7 @@ public sealed partial class CharacterLoadoutRequirement : CharacterRequirement
("loadouts", $"[color={color}]{string.Join($"[/color], [color={color}]",
Loadouts.Select(l => Loc.GetString($"loadout-name-{l}")))}[/color]"));
return Loadouts.Any(l => profile.LoadoutPreferences.Select(l => l.LoadoutName).Contains(l.ToString()));
return Loadouts.Any(l => profile.LoadoutPreferences.ContainsKey(l.ToString())); // WWDP EDIT
}
}

View File

@@ -53,10 +53,11 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
private HashSet<ProtoId<TraitPrototype>> _traitPreferences = new();
/// <see cref="_loadoutPreferences"/>
public HashSet<LoadoutPreference> LoadoutPreferences => _loadoutPreferences;
public Dictionary<string, Loadout> LoadoutPreferences => _loadoutPreferences; // WWDP EDIT
public IEnumerable<Loadout> LoadoutPreferencesList => _loadoutPreferences.Values; // WWDP EDIT
[DataField]
private HashSet<LoadoutPreference> _loadoutPreferences = new();
private Dictionary<string, Loadout> _loadoutPreferences = new(); // WWDP EDIT
[DataField]
public string Name { get; set; } = "John Doe";
@@ -184,7 +185,7 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
PreferenceUnavailableMode preferenceUnavailable,
HashSet<ProtoId<AntagPrototype>> antagPreferences,
HashSet<ProtoId<TraitPrototype>> traitPreferences,
HashSet<LoadoutPreference> loadoutPreferences)
Dictionary<string, Loadout> loadoutPreferences) // WWDP EDIT
{
Name = name;
FlavorText = flavortext;
@@ -264,7 +265,7 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
other.PreferenceUnavailable,
new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
new HashSet<LoadoutPreference>(other.LoadoutPreferences))
new Dictionary<string, Loadout>(other.LoadoutPreferences)) // WWDP EDIT
{
}
@@ -495,33 +496,14 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
return new(this) { _traitPreferences = list };
}
public HumanoidCharacterProfile WithLoadoutPreference(
string loadoutId,
bool pref,
string? customName = null,
string? customDescription = null,
string? customContent = null, // WD EDIT
string? customColor = null,
bool? customHeirloom = null)
// WWDP EDIT START
// I'll rip the hands off whoever coded this piece of shit named Loadouts
public HumanoidCharacterProfile WithLoadoutPreference(List<Loadout> loadouts)
{
// WD EDIT START
if (customContent is { Length: > MaxCustomContentLength, })
{
var truncated = customContent.AsSpan(0, MaxCustomContentLength);
while (truncated.Length > 0 && char.IsLowSurrogate(truncated[^1]))
truncated = truncated[..^1];
customContent = truncated.ToString();
}
// WD EDIT END
var list = new HashSet<LoadoutPreference>(_loadoutPreferences);
list.RemoveWhere(l => l.LoadoutName == loadoutId);
if (pref)
list.Add(new(loadoutId, customName, customDescription, customContent, customColor, customHeirloom) { Selected = pref }); // WD EDIT
return new HumanoidCharacterProfile(this) { _loadoutPreferences = list };
var dictionary = loadouts.ToDictionary(p => p.LoadoutName);
return new(this) { _loadoutPreferences = dictionary };
}
// WWDP EDIT END
public string Summary =>
Loc.GetString(
@@ -694,8 +676,7 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
.ToList();
var loadouts = LoadoutPreferences
.Where(l => prototypeManager.HasIndex<LoadoutPrototype>(l.LoadoutName))
.Distinct()
.Where(l => prototypeManager.HasIndex<LoadoutPrototype>(l.Key))
.ToList();
Name = name;
@@ -724,7 +705,6 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
_traitPreferences.UnionWith(traits);
_loadoutPreferences.Clear();
_loadoutPreferences.UnionWith(loadouts);
// WD EDIT START
prototypeManager.TryIndex<TTSVoicePrototype>(Voice, out var voice);
@@ -734,25 +714,26 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
if(!CanHaveBark(prototypeManager, collection))
BarkVoice = SharedHumanoidAppearanceSystem.DefaultBarkVoice;
for (var i = 0; i < loadouts.Count; i++)
foreach (var (key, loadout) in loadouts)
{
var loadout = loadouts[i];
if (loadout.CustomContent is not { Length: > MaxCustomContentLength, })
continue;
var truncated = loadout.CustomContent.AsSpan(0, MaxCustomContentLength);
while (truncated.Length > 0 && char.IsLowSurrogate(truncated[^1]))
truncated = truncated[..^1];
if (loadout.CustomContent is not { Length: > MaxCustomContentLength, })
{
_loadoutPreferences[key] = loadout;
continue;
}
var truncated = loadout.CustomContent.AsSpan(0, MaxCustomContentLength);
while (truncated.Length > 0 && char.IsLowSurrogate(truncated[^1]))
truncated = truncated[..^1];
var truncatedLoadout = new LoadoutPreference(
loadout.LoadoutName,
loadout.CustomName,
loadout.CustomDescription,
truncated.ToString(),
loadout.CustomColorTint,
loadout.CustomHeirloom)
{ Selected = loadout.Selected, };
var truncatedLoadout = new Loadout(
loadout.LoadoutName,
loadout.CustomName,
loadout.CustomDescription,
truncated.ToString(),
loadout.CustomColorTint,
loadout.CustomHeirloom);
loadouts[i] = truncatedLoadout;
_loadoutPreferences[key] = truncatedLoadout;
}
// WD EDIT END
}

View File

@@ -48,7 +48,7 @@ public sealed partial class CharacterItemGroupItem
p => protoMan.Index<TraitPrototype>((string) p).ID == ID);
return value != null;
case "loadout":
return profile.LoadoutPreferences.TryFirstOrDefault(
return profile.LoadoutPreferencesList.TryFirstOrDefault( // WWDP EDIT
p => protoMan.Index<LoadoutPrototype>(((Loadout) p).LoadoutName).ID == ID, out value);
default:
DebugTools.Assert($"Invalid CharacterItemGroupItem Type: {Type}");

View File

@@ -1,4 +1,8 @@
# Combat Maid
# loadout menu
loadout-error-prototype-not-found = Loadout prototype not found.
loadout-error-too-expensive = You cannot afford this item.
# Combat Maid
loadout-category-JobsCommandMaid = Maid
character-item-group-LoadoutMaidHats = Maid Hats

View File

@@ -1,4 +1,8 @@
# Combat Maid
# loadout menu
loadout-error-prototype-not-found = Прототип лоадаута не найден
loadout-error-too-expensive = Данный предмет вам не по карману!
# Combat Maid
loadout-category-JobsCommandMaid = Горничная
character-item-group-LoadoutMaidHats = Головные уборы горничной

View File

@@ -504,21 +504,21 @@ Global
{41B450C0-A361-4CD7-8121-7072B8995CFC} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{7B9472D3-79D4-48D1-9B22-BCDE518FE842} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{1FAE651D-29D8-437A-9864-47CE0D180016} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{3CFEB7DB-12C6-46F3-89FC-1450F3016FFA} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{8922428F-17C3-47A7-BFE9-570DEB2464DA} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{16F7DE32-0186-44B9-9345-0C20D1BF2422} = {AFF53804-115F-4E67-B81F-26265EA27880}
{AFF53804-115F-4E67-B81F-26265EA27880} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{23F09C45-950E-4DB7-A465-E937450FF008} = {AFF53804-115F-4E67-B81F-26265EA27880}
{440426C1-8DCA-43F6-967F-94439B8DAF47} = {AFF53804-115F-4E67-B81F-26265EA27880}
{88B0FC0F-7209-40E2-AF16-EB90AF727C5B} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{A3C5B00A-D232-4A01-B82E-B0E58BFD5C12} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{8A21C7CA-2EB8-40E5-8043-33582C06D139} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{952AAF2A-DF63-4A7D-8094-3453893EBA80} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{A965CB3B-FD31-44AF-8872-85ABA436098D} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{7844DA69-B0F0-49FB-A05E-ECA37372277A} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{3CFEB7DB-12C6-46F3-89FC-1450F3016FFA} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{07CA34A1-1D37-4771-A2E3-495A1044AE0B} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{88B0FC0F-7209-40E2-AF16-EB90AF727C5B} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{83F510FE-9B50-4D96-AFAB-CC13998D6AFE} = {7844DA69-B0F0-49FB-A05E-ECA37372277A}
{7844DA69-B0F0-49FB-A05E-ECA37372277A} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{8B35C796-2DCD-4B48-B159-689C0796B6A8} = {83B4CBBA-547A-42F0-A7CD-8A67D93196CE}
{C310B97E-ECC2-44DA-BABE-012E11F136BC} = {1BED7A6E-3F61-47AE-AED8-1176F241CC1E}
{127EE7A3-F1DE-45D0-8255-D0183B4FA42E} = {1BED7A6E-3F61-47AE-AED8-1176F241CC1E}