Merge pull request #1122 from DeltaV-Station/2024/04/21-loadouts

Loadouts
This commit is contained in:
Null 2024-05-15 14:48:11 +02:00 committed by GitHub
commit f9ec70d258
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
220 changed files with 16219 additions and 1669 deletions

View File

@ -58,7 +58,7 @@ public class SpawnEquipDeleteBenchmark
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
_spawnSys.EquipStartingGear(_entity, _gear, null);
_spawnSys.EquipStartingGear(_entity, _gear);
server.EntMan.DeleteEntity(_entity);
}
});

View File

@ -26,6 +26,9 @@
<ItemGroup>
<Folder Include="Spawners\" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Lobby\UI\LobbyCharacterPreviewPanel.cs" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
</Project>

View File

@ -21,6 +21,7 @@ using Content.Shared.Module;
using Content.Client.Guidebook;
using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking;
namespace Content.Client.IoC
@ -29,26 +30,29 @@ namespace Content.Client.IoC
{
public static void Register()
{
IoCManager.Register<IParallaxManager, ParallaxManager>();
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
IoCManager.Register<IStylesheetManager, StylesheetManager>();
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
IoCManager.Register<FullscreenHook, FullscreenHook>();
IoCManager.Register<IClickMapManager, ClickMapManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<ChangelogManager, ChangelogManager>();
IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<ViewportManager, ViewportManager>();
IoCManager.Register<ISharedAdminLogManager, SharedAdminLogManager>();
IoCManager.Register<GhostKickManager>();
IoCManager.Register<ExtendedDisconnectInformationManager>();
IoCManager.Register<JobRequirementsManager>();
IoCManager.Register<DocumentParsingManager>();
IoCManager.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
var collection = IoCManager.Instance!;
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
collection.Register<IStylesheetManager, StylesheetManager>();
collection.Register<IScreenshotHook, ScreenshotHook>();
collection.Register<FullscreenHook, FullscreenHook>();
collection.Register<IClickMapManager, ClickMapManager>();
collection.Register<IClientAdminManager, ClientAdminManager>();
collection.Register<ISharedAdminManager, ClientAdminManager>();
collection.Register<EuiManager, EuiManager>();
collection.Register<IVoteManager, VoteManager>();
collection.Register<ChangelogManager, ChangelogManager>();
collection.Register<RulesManager, RulesManager>();
collection.Register<ViewportManager, ViewportManager>();
collection.Register<ISharedAdminLogManager, SharedAdminLogManager>();
collection.Register<GhostKickManager>();
collection.Register<ExtendedDisconnectInformationManager>();
collection.Register<JobRequirementsManager>();
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
}
}
}

View File

@ -64,13 +64,19 @@ namespace Content.Client.Lobby
_characterSetup.CloseButton.OnPressed += _ =>
{
// Reset sliders etc.
_characterSetup?.UpdateControls();
var controller = _userInterfaceManager.GetUIController<LobbyUIController>();
controller.SetClothes(true);
controller.UpdateProfile();
_lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
};
_characterSetup.SaveButton.OnPressed += _ =>
{
_characterSetup.Save();
_lobby.CharacterPreview.UpdateUI();
_userInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
};
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
@ -84,10 +90,6 @@ namespace Content.Client.Lobby
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
_gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
_lobby.CharacterPreview.UpdateUI();
}
protected override void Shutdown()
@ -109,13 +111,6 @@ namespace Content.Client.Lobby
_characterSetup?.Dispose();
_characterSetup = null;
_preferencesManager.OnServerDataLoaded -= PreferencesDataLoaded;
}
private void PreferencesDataLoaded()
{
_lobby?.CharacterPreview.UpdateUI();
}
private void OnSetupPressed(BaseButton.ButtonEventArgs args)

View File

@ -0,0 +1,286 @@
using System.Linq;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Lobby.UI;
using Content.Client.Preferences;
using Content.Client.Preferences.UI;
using Content.Client.Station;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby;
public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
{
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
private LobbyCharacterPreviewPanel? _previewPanel;
private bool _showClothes = true;
/*
* Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
* that is shared too.
*/
/// <summary>
/// Preview dummy for role gear.
/// </summary>
private EntityUid? _previewDummy;
/// <summary>
/// If we currently have a job prototype selected.
/// </summary>
private JobPrototype? _dummyJob;
// TODO: Load the species directly and don't update entity ever.
public event Action<EntityUid>? PreviewDummyUpdated;
private HumanoidCharacterProfile? _profile;
public override void Initialize()
{
base.Initialize();
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
}
private void PreferencesDataLoaded()
{
UpdateProfile();
}
public void OnStateEntered(LobbyState state)
{
}
public void OnStateExited(LobbyState state)
{
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
}
public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
{
_previewPanel = panel;
ReloadProfile();
}
public void SetClothes(bool value)
{
if (_showClothes == value)
return;
_showClothes = value;
ReloadCharacterUI();
}
public void SetDummyJob(JobPrototype? job)
{
_dummyJob = job;
ReloadCharacterUI();
}
/// <summary>
/// Updates the character only with the specified profile change.
/// </summary>
public void ReloadProfile()
{
// Test moment
if (_profile == null || _stateManager.CurrentState is not LobbyState)
return;
// Ignore job clothes and the likes so we don't spam entities out every frame of color changes.
var previewDummy = EnsurePreviewDummy(_profile);
_humanoid.LoadProfile(previewDummy, _profile);
}
/// <summary>
/// Updates the currently selected character's preview.
/// </summary>
public void ReloadCharacterUI()
{
// Test moment
if (_profile == null || _stateManager.CurrentState is not LobbyState)
return;
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
_previewDummy = EnsurePreviewDummy(_profile);
_previewPanel?.SetSprite(_previewDummy.Value);
_previewPanel?.SetSummaryText(_profile.Summary);
_humanoid.LoadProfile(_previewDummy.Value, _profile);
if (_showClothes)
GiveDummyJobClothesLoadout(_previewDummy.Value, _profile);
}
/// <summary>
/// Updates character profile to the default.
/// </summary>
public void UpdateProfile()
{
if (!_preferencesManager.ServerDataLoaded)
{
_profile = null;
return;
}
if (_preferencesManager.Preferences?.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)
{
_profile = selectedCharacter;
_previewPanel?.SetLoaded(true);
}
else
{
_previewPanel?.SetSummaryText(string.Empty);
_previewPanel?.SetLoaded(false);
}
ReloadCharacterUI();
}
public void UpdateProfile(HumanoidCharacterProfile? profile)
{
if (_profile?.Equals(profile) == true)
return;
if (_stateManager.CurrentState is not LobbyState)
return;
_profile = profile;
}
private EntityUid EnsurePreviewDummy(HumanoidCharacterProfile profile)
{
if (_previewDummy != null)
return _previewDummy.Value;
_previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(profile.Species).DollPrototype, MapCoordinates.Nullspace);
PreviewDummyUpdated?.Invoke(_previewDummy.Value);
return _previewDummy.Value;
}
/// <summary>
/// Applies the highest priority job's clothes to the dummy.
/// </summary>
public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
{
var job = _dummyJob ?? GetPreferredJob(profile);
GiveDummyJobClothes(dummy, profile, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager);
GiveDummyLoadout(dummy, loadout);
}
}
/// <summary>
/// Gets the highest priority job for the profile.
/// </summary>
public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
{
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
return _prototypeManager.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
}
public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
{
if (roleLoadout == null)
return;
foreach (var group in roleLoadout.SelectedLoadouts.Values)
{
foreach (var loadout in group)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
_spawn.EquipStartingGear(uid, _prototypeManager.Index(loadoutProto.Equipment));
}
}
}
/// <summary>
/// Applies the specified job's clothes to the dummy.
/// </summary>
public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
{
if (!_inventory.TryGetSlots(dummy, out var slots))
return;
// Apply loadout
if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
{
foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
{
foreach (var loadout in loadouts)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
// TODO: Need some way to apply starting gear to an entity coz holy fucking shit dude.
var loadoutGear = _prototypeManager.Index(loadoutProto.Equipment);
foreach (var slot in slots)
{
var itemType = loadoutGear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
if (job.StartingGear == null)
return;
var gear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
public EntityUid? GetPreviewDummy()
{
return _previewDummy;
}
}

View File

@ -1,166 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.Alerts;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Preferences;
using Content.Client.UserInterface.Controls;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Lobby.UI
{
public sealed class LobbyCharacterPreviewPanel : Control
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private EntityUid? _previewDummy;
private readonly Label _summaryLabel;
private readonly BoxContainer _loaded;
private readonly BoxContainer _viewBox;
private readonly Label _unloaded;
public LobbyCharacterPreviewPanel()
{
IoCManager.InjectDependencies(this);
var header = new NanoHeading
{
Text = Loc.GetString("lobby-character-preview-panel-header")
};
CharacterSetupButton = new Button
{
Text = Loc.GetString("lobby-character-preview-panel-character-setup-button"),
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(0, 5, 0, 0),
};
_summaryLabel = new Label
{
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(3, 3),
};
var vBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
};
_unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
_loaded = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Visible = false
};
_viewBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Center,
};
var _vSpacer = new VSpacer();
_loaded.AddChild(_summaryLabel);
_loaded.AddChild(_viewBox);
_loaded.AddChild(_vSpacer);
_loaded.AddChild(CharacterSetupButton);
vBox.AddChild(header);
vBox.AddChild(_loaded);
vBox.AddChild(_unloaded);
AddChild(vBox);
UpdateUI();
}
public Button CharacterSetupButton { get; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
if (_previewDummy != null) _entityManager.DeleteEntity(_previewDummy.Value);
_previewDummy = default;
}
public void UpdateUI()
{
if (!_preferencesManager.ServerDataLoaded)
{
_loaded.Visible = false;
_unloaded.Visible = true;
}
else
{
_loaded.Visible = true;
_unloaded.Visible = false;
if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
{
_summaryLabel.Text = string.Empty;
}
else
{
_previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
_viewBox.DisposeAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(4f, 4f),
MaxSize = new Vector2(112, 112),
Stretch = SpriteView.StretchMode.Fill,
};
spriteView.SetEntity(_previewDummy.Value);
_viewBox.AddChild(spriteView);
_summaryLabel.Text = selectedCharacter.Summary;
_entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
}
}
}
public static void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile)
{
var protoMan = IoCManager.Resolve<IPrototypeManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var invSystem = EntitySystem.Get<ClientInventorySystem>();
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
var job = protoMan.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
if (job.StartingGear != null && invSystem.TryGetSlots(dummy, out var slots))
{
var gear = protoMan.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name, profile);
if (invSystem.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
entMan.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = entMan.SpawnEntity(itemType, MapCoordinates.Nullspace);
invSystem.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
}

View File

@ -0,0 +1,22 @@
<Control
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Name="VBox" Orientation="Vertical">
<controls:NanoHeading Name="Header" Text="{Loc 'lobby-character-preview-panel-header'}">
</controls:NanoHeading>
<BoxContainer Name="Loaded" Orientation="Vertical"
Visible="False">
<Label Name="Summary" HorizontalAlignment="Center" Margin="3 3"/>
<BoxContainer Name="ViewBox" Orientation="Horizontal" HorizontalAlignment="Center">
</BoxContainer>
<controls:VSpacer/>
<Button Name="CharacterSetup" Text="{Loc 'lobby-character-preview-panel-character-setup-button'}"
HorizontalAlignment="Center"
Margin="0 5 0 0"/>
</BoxContainer>
<Label Name="Unloaded" Text="{Loc 'lobby-character-preview-panel-unloaded-preferences-label'}"/>
</BoxContainer>
</Control>

View File

@ -0,0 +1,45 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences]
public sealed partial class LobbyCharacterPreviewPanel : Control
{
public Button CharacterSetupButton => CharacterSetup;
public LobbyCharacterPreviewPanel()
{
RobustXamlLoader.Load(this);
UserInterfaceManager.GetUIController<LobbyUIController>().SetPreviewPanel(this);
}
public void SetLoaded(bool value)
{
Loaded.Visible = value;
Unloaded.Visible = !value;
}
public void SetSummaryText(string value)
{
Summary.Text = string.Empty;
}
public void SetSprite(EntityUid uid)
{
ViewBox.DisposeAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(4f, 4f),
MaxSize = new Vector2(112, 112),
Stretch = SpriteView.StretchMode.Fill,
};
spriteView.SetEntity(uid);
ViewBox.AddChild(spriteView);
}
}

View File

@ -1,23 +1,9 @@
using Content.Client.Chat.UI;
using Content.Client.Info;
using Content.Client.Message;
using Content.Client.Preferences;
using Content.Client.Preferences.UI;
using Content.Client.UserInterface.Screens;
using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Lobby.UI
{

View File

@ -7,12 +7,13 @@ using Robust.Client;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Players.PlayTimeTracking;
public sealed partial class JobRequirementsManager
public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
@ -134,5 +135,13 @@ public sealed partial class JobRequirementsManager
}
}
public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
{
if (session != _playerManager.LocalSession)
{
return new Dictionary<string, TimeSpan>();
}
return _roles;
}
}

View File

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Preferences;
using Robust.Client;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@ -20,8 +18,7 @@ namespace Content.Client.Preferences
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public event Action? OnServerDataLoaded;
@ -64,7 +61,8 @@ namespace Content.Client.Preferences
public void UpdateCharacter(ICharacterProfile profile, int slot)
{
profile.EnsureValid(_cfg, _prototypes);
var collection = IoCManager.Instance!;
profile.EnsureValid(_playerManager.LocalSession!, collection);
var characters = new Dictionary<int, ICharacterProfile>(Preferences.Characters) {[slot] = profile};
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor);
var msg = new MsgUpdateCharacter

View File

@ -0,0 +1,41 @@
using Content.Client.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Preferences.UI;
public sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
// Not supported yet get fucked.
Setup(null, items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}

View File

@ -40,7 +40,7 @@
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Name="CharEditor" />
<BoxContainer Name="CharEditor" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@ -3,27 +3,23 @@ using System.Numerics;
using Content.Client.Humanoid;
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
using Content.Client.Lobby.UI;
using Content.Client.Lobby;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
@ -36,7 +32,6 @@ namespace Content.Client.Preferences.UI
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager;
private readonly IConfigurationManager _configurationManager;
private readonly Button _createNewCharacterButton;
private readonly HumanoidProfileEditor _humanoidProfileEditor;
@ -51,7 +46,6 @@ namespace Content.Client.Preferences.UI
_entityManager = entityManager;
_prototypeManager = prototypeManager;
_preferencesManager = preferencesManager;
_configurationManager = configurationManager;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
@ -74,7 +68,7 @@ namespace Content.Client.Preferences.UI
args.Event.Handle();
};
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, entityManager, configurationManager);
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
_humanoidProfileEditor.OnProfileChanged += ProfileChanged;
CharEditor.AddChild(_humanoidProfileEditor);
@ -103,6 +97,12 @@ namespace Content.Client.Preferences.UI
UpdateUI();
}
public void UpdateControls()
{
// Reset sliders etc. upon going going back to GUI.
_humanoidProfileEditor.LoadServerData();
}
private void UpdateUI()
{
var numberOfFullSlots = 0;
@ -120,11 +120,6 @@ namespace Content.Client.Preferences.UI
foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
{
if (character is null)
{
continue;
}
numberOfFullSlots++;
var characterPickerButton = new CharacterPickerButton(_entityManager,
_preferencesManager,
@ -140,6 +135,9 @@ namespace Content.Client.Preferences.UI
_humanoidProfileEditor.CharacterSlot = characterIndexCopy;
_humanoidProfileEditor.UpdateControls();
_preferencesManager.SelectCharacter(character);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(_humanoidProfileEditor.Profile);
controller.ReloadCharacterUI();
UpdateUI();
args.Event.Handle();
};
@ -148,8 +146,12 @@ namespace Content.Client.Preferences.UI
_createNewCharacterButton.Disabled =
numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
Characters.AddChild(_createNewCharacterButton);
// TODO: Move this shit to the Lobby UI controller
}
/// <summary>
/// Shows individual characters on the side of the character GUI.
/// </summary>
private sealed class CharacterPickerButton : ContainerButton
{
private EntityUid _previewDummy;
@ -180,7 +182,15 @@ namespace Content.Client.Preferences.UI
if (humanoid != null)
{
LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy, humanoid);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
var job = controller.GetPreferredJob(humanoid);
controller.GiveDummyJobClothes(_previewDummy, humanoid, job);
if (prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager);
controller.GiveDummyLoadout(_previewDummy, loadout);
}
}
var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;

View File

@ -0,0 +1,11 @@
<PanelContainer
xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2F2F35"
ContentMarginTopOverride="10"
ContentMarginBottomOverride="10"
ContentMarginLeftOverride="10"
ContentMarginRightOverride="10"/>
</PanelContainer.PanelOverride>
</PanelContainer>

View File

@ -0,0 +1,14 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class HighlightedContainer : PanelContainer
{
public HighlightedContainer()
{
RobustXamlLoader.Load(this);
}
}

View File

@ -5,8 +5,6 @@ namespace Content.Client.Preferences.UI
{
public sealed partial class HumanoidProfileEditor
{
private readonly IPrototypeManager _prototypeManager;
private void RandomizeEverything()
{
Profile = HumanoidCharacterProfile.Random();

View File

@ -1,11 +1,11 @@
<Control xmlns="https://spacestation14.io"
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prefUi="clr-namespace:Content.Client.Preferences.UI"
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<BoxContainer Orientation="Horizontal">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
HorizontalExpand="True">
<!-- Left side -->
<BoxContainer Orientation="Vertical" Margin="10 10 10 10">
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
<!-- Middle container -->
<BoxContainer Orientation="Horizontal" SeparationOverride="10">
<!-- Name box-->
@ -58,7 +58,9 @@
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-species-label'}" />
<Control HorizontalExpand="True"/>
<TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3" VerticalAlignment="Center"></TextureButton>
<TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3"
VerticalAlignment="Center"
ToolTip="{Loc 'humanoid-profile-editor-guidebook-button-tooltip'}"/>
<OptionButton Name="CSpeciesButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Age -->
@ -85,18 +87,6 @@
<Control HorizontalExpand="True"/>
<Button Name="ShowClothes" Pressed="True" ToggleMode="True" Text="{Loc 'humanoid-profile-editor-clothing-show'}" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Clothing -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-clothing-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="CClothingButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Backpack -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-backpack-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="CBackpackButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Spawn Priority -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-spawn-priority-label'}" />
@ -151,7 +141,7 @@
</TabContainer>
</BoxContainer>
<!-- Right side -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Center">
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<SpriteView Name="CSpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
<Button Name="CSpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
@ -159,5 +149,4 @@
<Button Name="CSpriteRotateRight" Text="▶" StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</Control>
</BoxContainer>

View File

@ -2,69 +2,48 @@ using System.Linq;
using System.Numerics;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Lobby.UI;
using Content.Client.Lobby;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Preferences.UI
{
public sealed class HighlightedContainer : PanelContainer
{
public HighlightedContainer()
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = new Color(47, 47, 53),
ContentMarginTopOverride = 10,
ContentMarginBottomOverride = 10,
ContentMarginLeftOverride = 10,
ContentMarginRightOverride = 10
};
}
}
[GenerateTypedNameReferences]
public sealed partial class HumanoidProfileEditor : Control
public sealed partial class HumanoidProfileEditor : BoxContainer
{
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entMan;
private readonly IConfigurationManager _configurationManager;
private readonly IPrototypeManager _prototypeManager;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private LineEdit _ageEdit => CAgeEdit;
private LineEdit _nameEdit => CNameEdit;
private TextEdit _flavorTextEdit = null!;
private TextEdit? _flavorTextEdit;
private Button _nameRandomButton => CNameRandomize;
private Button _randomizeEverythingButton => CRandomizeEverything;
private RichTextLabel _warningLabel => CWarningLabel;
@ -72,8 +51,6 @@ namespace Content.Client.Preferences.UI
private OptionButton _sexButton => CSexButton;
private OptionButton _genderButton => CPronounsButton;
private Slider _skinColor => CSkin;
private OptionButton _clothingButton => CClothingButton;
private OptionButton _backpackButton => CBackpackButton;
private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
private SingleMarkingPicker _hairPicker => CHairStylePicker;
private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
@ -88,44 +65,39 @@ namespace Content.Client.Preferences.UI
private readonly Dictionary<string, BoxContainer> _jobCategories;
// Mildly hacky, as I don't trust prototype order to stay consistent and don't want the UI to break should a new one get added mid-edit. --moony
private readonly List<SpeciesPrototype> _speciesList;
private readonly List<AntagPreferenceSelector> _antagPreferences;
private readonly List<AntagPreferenceSelector> _antagPreferences = new();
private readonly List<TraitPreferenceSelector> _traitPreferences;
private SpriteView _previewSpriteView => CSpriteView;
private Button _previewRotateLeftButton => CSpriteRotateLeft;
private Button _previewRotateRightButton => CSpriteRotateRight;
private Direction _previewRotation = Direction.North;
private EntityUid? _previewDummy;
private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer;
private ColorSelectorSliders _rgbSkinColorSelector;
private bool _isDirty;
private bool _needUpdatePreview;
public int CharacterSlot;
public HumanoidCharacterProfile? Profile;
private MarkingSet _markingSet = new(); // storing this here feels iffy but a few things need it this high up
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager,
IEntityManager entityManager, IConfigurationManager configurationManager)
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IConfigurationManager configurationManager)
{
RobustXamlLoader.Load(this);
_prototypeManager = prototypeManager;
_entMan = entityManager;
_preferencesManager = preferencesManager;
_configurationManager = configurationManager;
_markingManager = IoCManager.Resolve<MarkingManager>();
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.PreviewDummyUpdated += OnDummyUpdate;
SpeciesInfoButton.ToolTip = Loc.GetString("humanoid-profile-editor-guidebook-button-tooltip");
_previewSpriteView.SetEntity(controller.GetPreviewDummy());
#region Left
#region Randomize
#endregion Randomize
#region Name
_nameEdit.OnTextChanged += args => { SetName(args.Text); };
@ -139,8 +111,6 @@ namespace Content.Client.Preferences.UI
_tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
ShowClothes.OnPressed += ToggleClothes;
#region Sex
_sexButton.OnItemSelected += args =>
@ -220,7 +190,7 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairStyleName(newStyle.id));
IsDirty = true;
SetDirty();
};
_hairPicker.OnColorChanged += newColor =>
@ -230,7 +200,7 @@ namespace Content.Client.Preferences.UI
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsHair();
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnMarkingSelect += newStyle =>
@ -239,7 +209,7 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnColorChanged += newColor =>
@ -249,7 +219,7 @@ namespace Content.Client.Preferences.UI
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsFacialHair();
IsDirty = true;
SetDirty();
};
_hairPicker.OnSlotRemove += _ =>
@ -261,7 +231,7 @@ namespace Content.Client.Preferences.UI
);
UpdateHairPickers();
UpdateCMarkingsHair();
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnSlotRemove += _ =>
@ -273,7 +243,7 @@ namespace Content.Client.Preferences.UI
);
UpdateHairPickers();
UpdateCMarkingsFacialHair();
IsDirty = true;
SetDirty();
};
_hairPicker.OnSlotAdd += delegate()
@ -293,7 +263,7 @@ namespace Content.Client.Preferences.UI
UpdateHairPickers();
UpdateCMarkingsHair();
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnSlotAdd += delegate()
@ -313,38 +283,11 @@ namespace Content.Client.Preferences.UI
UpdateHairPickers();
UpdateCMarkingsFacialHair();
IsDirty = true;
SetDirty();
};
#endregion Hair
#region Clothing
_clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit);
_clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt);
_clothingButton.OnItemSelected += args =>
{
_clothingButton.SelectId(args.Id);
SetClothing((ClothingPreference) args.Id);
};
#endregion Clothing
#region Backpack
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack);
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel);
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag);
_backpackButton.OnItemSelected += args =>
{
_backpackButton.SelectId(args.Id);
SetBackpack((BackpackPreference) args.Id);
};
#endregion Backpack
#region SpawnPriority
foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
@ -369,7 +312,7 @@ namespace Content.Client.Preferences.UI
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithEyeColor(newColor));
CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
IsDirty = true;
SetDirty();
};
#endregion Eyes
@ -393,46 +336,22 @@ namespace Content.Client.Preferences.UI
_preferenceUnavailableButton.SelectId(args.Id);
Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
IsDirty = true;
SetDirty();
};
_jobPriorities = new List<JobPrioritySelector>();
_jobCategories = new Dictionary<string, BoxContainer>();
_requirements = IoCManager.Resolve<JobRequirementsManager>();
// TODO: Move this to the LobbyUIController instead of being spaghetti everywhere.
_requirements.Updated += UpdateAntagRequirements;
_requirements.Updated += UpdateRoleRequirements;
UpdateAntagRequirements();
UpdateRoleRequirements();
#endregion Jobs
#region Antags
_tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
_antagPreferences = new List<AntagPreferenceSelector>();
foreach (var antag in prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var selector = new AntagPreferenceSelector(antag);
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
if (selector.Disabled)
{
Profile = Profile?.WithAntagPreference(antag.ID, false);
IsDirty = true;
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithAntagPreference(antag.ID, preference);
IsDirty = true;
};
}
#endregion Antags
#region Traits
var traits = prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
@ -450,7 +369,7 @@ namespace Content.Client.Preferences.UI
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithTraitPreference(trait.ID, preference);
IsDirty = true;
SetDirty();
};
}
}
@ -483,7 +402,7 @@ namespace Content.Client.Preferences.UI
#region FlavorText
if (_configurationManager.GetCVar(CCVars.FlavorText))
if (configurationManager.GetCVar(CCVars.FlavorText))
{
var flavorText = new FlavorText.FlavorText();
_tabContainer.AddChild(flavorText);
@ -500,22 +419,14 @@ namespace Content.Client.Preferences.UI
_previewRotateLeftButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCw();
_needUpdatePreview = true;
SetPreviewRotation(_previewRotation);
};
_previewRotateRightButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCcw();
_needUpdatePreview = true;
SetPreviewRotation(_previewRotation);
};
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy!.Value);
_previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
_previewSpriteView.SetEntity(_previewDummy);
#endregion Dummy
#endregion Left
@ -525,6 +436,13 @@ namespace Content.Client.Preferences.UI
LoadServerData();
}
ShowClothes.OnToggled += args =>
{
var lobby = UserInterfaceManager.GetUIController<LobbyUIController>();
lobby.SetClothes(args.Pressed);
SetDirty();
};
preferencesManager.OnServerDataLoaded += LoadServerData;
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
@ -532,28 +450,69 @@ namespace Content.Client.Preferences.UI
UpdateSpeciesGuidebookIcon();
IsDirty = false;
controller.UpdateProfile();
}
private void SetDirty()
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadCharacterUI();
IsDirty = true;
}
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
{
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = "Species";
var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
page = species;
if (_prototypeManager.TryIndex<GuideEntryPrototype>("Species", out var guideRoot))
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<string, GuideEntry>();
dict.Add("Species", guideRoot);
dict.Add(DefaultSpeciesGuidebook, guideRoot);
//TODO: Don't close the guidebook if its already open, just go to the correct page
guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
}
}
private void ToggleClothes(BaseButton.ButtonEventArgs obj)
private void OnDummyUpdate(EntityUid value)
{
RebuildSpriteView();
_previewSpriteView.SetEntity(value);
}
private void UpdateAntagRequirements()
{
_antagList.DisposeAllChildren();
_antagPreferences.Clear();
var btnGroup = new ButtonGroup();
foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var selector = new AntagPreferenceSelector(antag, btnGroup)
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
if (selector.Disabled)
{
Profile = Profile?.WithAntagPreference(antag.ID, false);
SetDirty();
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithAntagPreference(antag.ID, preference);
SetDirty();
};
}
}
private void UpdateRoleRequirements()
@ -614,10 +573,19 @@ namespace Content.Client.Preferences.UI
.Where(job => job.SetPreference)
.ToArray();
Array.Sort(jobs, JobUIComparer.Instance);
var jobLoadoutGroup = new ButtonGroup();
foreach (var job in jobs)
{
var selector = new JobPrioritySelector(job, _prototypeManager);
RoleLoadout? loadout = null;
// Clone so we don't modify the underlying loadout.
Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
loadout = loadout?.Clone();
var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
if (!_requirements.IsAllowed(job, out var reason))
{
@ -626,12 +594,16 @@ namespace Content.Client.Preferences.UI
category.AddChild(selector);
_jobPriorities.Add(selector);
EnsureJobRequirementsValid(); // DeltaV
selector.LoadoutUpdated += args =>
{
Profile = Profile?.WithLoadout(args);
SetDirty();
};
selector.PriorityChanged += priority =>
{
Profile = Profile?.WithJobPriority(job.ID, priority);
IsDirty = true;
foreach (var jobSelector in _jobPriorities)
{
@ -647,6 +619,8 @@ namespace Content.Client.Preferences.UI
Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
}
}
SetDirty();
};
}
@ -658,35 +632,13 @@ namespace Content.Client.Preferences.UI
}
}
/// <summary>
/// DeltaV - Make sure that no invalid job priorities get through.
/// </summary>
private void EnsureJobRequirementsValid()
{
var changed = false;
foreach (var selector in _jobPriorities)
{
if (_requirements.IsAllowed(selector.Proto, out var _) || selector.Priority == JobPriority.Never)
continue;
selector.Priority = JobPriority.Never;
Profile = Profile?.WithJobPriority(selector.Proto.ID, JobPriority.Never);
changed = true;
}
if (!changed)
return;
_needUpdatePreview = true;
Save();
}
private void OnFlavorTextChange(string content)
{
if (Profile is null)
return;
Profile = Profile.WithFlavorText(content);
IsDirty = true;
SetDirty();
}
private void OnMarkingChange(MarkingSet markings)
@ -695,20 +647,12 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
_needUpdatePreview = true;
IsDirty = true;
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadProfile();
}
private void OnMarkingColorChange(List<Marking> markings)
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings));
IsDirty = true;
}
private void OnSkinColorOnValueChanged()
{
if (Profile is null) return;
@ -782,6 +726,9 @@ namespace Content.Client.Preferences.UI
}
IsDirty = true;
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadProfile();
}
protected override void Dispose(bool disposing)
@ -790,40 +737,27 @@ namespace Content.Client.Preferences.UI
if (!disposing)
return;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy.Value);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.PreviewDummyUpdated -= OnDummyUpdate;
_requirements.Updated -= UpdateAntagRequirements;
_requirements.Updated -= UpdateRoleRequirements;
_preferencesManager.OnServerDataLoaded -= LoadServerData;
}
private void RebuildSpriteView()
{
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy!.Value);
_previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
_previewSpriteView.SetEntity(_previewDummy);
_needUpdatePreview = true;
}
private void LoadServerData()
public void LoadServerData()
{
Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
UpdateAntagRequirements();
UpdateControls();
EnsureJobRequirementsValid(); // DeltaV
_needUpdatePreview = true;
ShowClothes.Pressed = true;
}
private void SetAge(int newAge)
{
Profile = Profile?.WithAge(newAge);
IsDirty = true;
SetDirty();
}
private void SetSex(Sex newSex)
@ -844,13 +778,13 @@ namespace Content.Client.Preferences.UI
}
UpdateGenderControls();
CMarkings.SetSex(newSex);
IsDirty = true;
SetDirty();
}
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
IsDirty = true;
SetDirty();
}
private void SetSpecies(string newSpecies)
@ -859,46 +793,34 @@ namespace Content.Client.Preferences.UI
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
UpdateSexControls(); // update sex for new species
RebuildSpriteView(); // they might have different inv so we need a new dummy
UpdateSpeciesGuidebookIcon();
IsDirty = true;
_needUpdatePreview = true;
SetDirty();
UpdatePreview();
}
private void SetName(string newName)
{
Profile = Profile?.WithName(newName);
IsDirty = true;
}
private void SetClothing(ClothingPreference newClothing)
{
Profile = Profile?.WithClothingPreference(newClothing);
IsDirty = true;
}
private void SetBackpack(BackpackPreference newBackpack)
{
Profile = Profile?.WithBackpackPreference(newBackpack);
IsDirty = true;
SetDirty();
}
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
{
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
IsDirty = true;
SetDirty();
}
public void Save()
{
IsDirty = false;
if (Profile != null)
{
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
OnProfileChanged?.Invoke(Profile, CharacterSlot);
_needUpdatePreview = true;
}
if (Profile == null)
return;
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
OnProfileChanged?.Invoke(Profile, CharacterSlot);
// Reset profile to default.
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateProfile();
}
private bool IsDirty
@ -907,7 +829,6 @@ namespace Content.Client.Preferences.UI
set
{
_isDirty = value;
_needUpdatePreview = true;
UpdateSaveButton();
}
}
@ -1039,7 +960,7 @@ namespace Content.Client.Preferences.UI
if (!_prototypeManager.HasIndex<GuideEntryPrototype>(species))
return;
var style = speciesProto.GuideBookIcon;
const string style = "SpeciesInfoDefault";
SpeciesInfoButton.StyleClasses.Add(style);
}
@ -1075,26 +996,6 @@ namespace Content.Client.Preferences.UI
_genderButton.SelectId((int) Profile.Gender);
}
private void UpdateClothingControls()
{
if (Profile == null)
{
return;
}
_clothingButton.SelectId((int) Profile.Clothing);
}
private void UpdateBackpackControls()
{
if (Profile == null)
{
return;
}
_backpackButton.SelectId((int) Profile.Backpack);
}
private void UpdateSpawnPriorityControls()
{
if (Profile == null)
@ -1224,13 +1125,13 @@ namespace Content.Client.Preferences.UI
if (Profile is null)
return;
var humanoid = _entMan.System<HumanoidAppearanceSystem>();
humanoid.LoadProfile(_previewDummy!.Value, Profile);
UserInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
SetPreviewRotation(_previewRotation);
}
if (ShowClothes.Pressed)
LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy!.Value, Profile);
_previewSpriteView.OverrideDirection = (Direction) ((int) _previewRotation % 4 * 2);
private void SetPreviewRotation(Direction direction)
{
_previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
}
public void UpdateControls()
@ -1242,17 +1143,16 @@ namespace Content.Client.Preferences.UI
UpdateGenderControls();
UpdateSkinColor();
UpdateSpecies();
UpdateClothingControls();
UpdateBackpackControls();
UpdateSpawnPriorityControls();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateLoadouts();
UpdateRoleRequirements();
UpdateJobPriorities();
UpdateAntagPreferences();
UpdateTraitPreferences();
UpdateMarkings();
RebuildSpriteView();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
@ -1260,17 +1160,6 @@ namespace Content.Client.Preferences.UI
_preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_needUpdatePreview)
{
UpdatePreview();
_needUpdatePreview = false;
}
}
private void UpdateJobPriorities()
{
foreach (var prioritySelector in _jobPriorities)
@ -1283,143 +1172,11 @@ namespace Content.Client.Preferences.UI
}
}
private abstract class RequirementsSelector<T> : Control
private void UpdateLoadouts()
{
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private StripeBack _lockStripe;
private Label _requirementsLabel;
protected RequirementsSelector(T proto)
foreach (var prioritySelector in _jobPriorities)
{
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
_requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
MouseFilter = MouseFilterMode.Stop,
Children =
{
_requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label()
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
var container = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
};
if (icon != null)
container.AddChild(icon);
container.AddChild(titleLabel);
container.AddChild(Options);
container.AddChild(_lockStripe);
AddChild(container);
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90
};
}
}
private sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan)
: base(proto)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
prioritySelector.CloseLoadout();
}
}
@ -1444,41 +1201,6 @@ namespace Content.Client.Preferences.UI
}
}
private sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto)
: base(proto)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
Setup(items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}
private sealed class TraitPreferenceSelector : Control
{
public TraitPrototype Trait { get; }

View File

@ -0,0 +1,46 @@
using System.Numerics;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
public sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(RoleLoadout? loadout, JobPrototype proto, ButtonGroup btnGroup, IPrototypeManager protoMan)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(loadout, items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
}
}

View File

@ -0,0 +1,15 @@
<BoxContainer Name="Container" xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Orientation="Horizontal"
HorizontalExpand="True"
MouseFilter="Ignore"
Margin="0 0 0 5">
<Button Name="SelectButton" ToggleMode="True" Margin="0 0 5 0" HorizontalExpand="True"/>
<PanelContainer SetSize="64 64" HorizontalAlignment="Right">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<SpriteView Name="Sprite" Scale="4 4" MouseFilter="Stop"/>
</PanelContainer>
</BoxContainer>

View File

@ -0,0 +1,74 @@
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated;
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.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutContainer : BoxContainer
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private readonly EntityUid? _entity;
public Button Select => SelectButton;
public LoadoutContainer(ProtoId<LoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SelectButton.Disabled = disabled;
if (disabled && reason != null)
{
var tooltip = new Tooltip();
tooltip.SetMessage(reason);
SelectButton.TooltipSupplier = _ => tooltip;
}
if (_protoManager.TryIndex(proto, out var loadProto))
{
var ent = _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
if (ent != null)
{
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
Sprite.TooltipSupplier = _ => spriteTooltip;
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(_entity);
}
public bool Pressed
{
get => SelectButton.Pressed;
set => SelectButton.Pressed = value;
}
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
}

View File

@ -0,0 +1,10 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
</PanelContainer>
<!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
<Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
</BoxContainer>

View File

@ -0,0 +1,93 @@
using System.Linq;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutGroupContainer : BoxContainer
{
private readonly LoadoutGroupPrototype _groupProto;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
_groupProto = groupProto;
RefreshLoadouts(loadout, session, collection);
}
/// <summary>
/// Updates button availabilities and buttons.
/// </summary>
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{
var protoMan = collection.Resolve<IPrototypeManager>();
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
RestrictionsContainer.DisposeAllChildren();
if (_groupProto.MinLimit > 0)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-min-limit", ("count", _groupProto.MinLimit)),
Margin = new Thickness(5, 0, 5, 5),
});
}
if (_groupProto.MaxLimit > 0)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-max-limit", ("count", _groupProto.MaxLimit)),
Margin = new Thickness(5, 0, 5, 5),
});
}
if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-points-limit", ("count", loadout.Points.Value), ("max", roleProto.Points.Value)),
Margin = new Thickness(5, 0, 5, 5),
});
}
LoadoutsContainer.DisposeAllChildren();
// Didn't use options because this is more robust in future.
var selected = loadout.SelectedLoadouts[_groupProto.ID];
foreach (var loadoutProto in _groupProto.Loadouts)
{
if (!protoMan.TryIndex(loadoutProto, out var loadProto))
continue;
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
var pressed = matchingLoadout != null;
var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason);
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
loadoutContainer.Select.Pressed = pressed;
loadoutContainer.Text = loadoutSystem.GetName(loadProto);
loadoutContainer.Select.OnPressed += args =>
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(loadoutProto);
else
OnLoadoutUnpressed?.Invoke(loadoutProto);
};
LoadoutsContainer.AddChild(loadoutContainer);
}
}
}

View File

@ -0,0 +1,10 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="800 800"
MinSize="800 64">
<VerticalTabContainer Name="LoadoutGroupsContainer"
VerticalExpand="True"
HorizontalExpand="True">
</VerticalTabContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,60 @@
using Content.Client.Lobby;
using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutWindow : FancyWindow
{
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
private List<LoadoutGroupContainer> _groups = new();
public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
var protoManager = collection.Resolve<IPrototypeManager>();
foreach (var group in proto.Groups)
{
if (!protoManager.TryIndex(group, out var groupProto))
continue;
var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection);
LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
_groups.Add(container);
container.OnLoadoutPressed += args =>
{
OnLoadoutPressed?.Invoke(group, args);
};
container.OnLoadoutUnpressed += args =>
{
OnLoadoutUnpressed?.Invoke(group, args);
};
}
}
public override void Close()
{
base.Close();
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(null);
}
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{
foreach (var group in _groups)
{
group.RefreshLoadouts(loadout, session, collection);
}
}
}

View File

@ -0,0 +1,222 @@
using System.Numerics;
using Content.Client.Lobby;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI;
public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototype
{
private ButtonGroup _loadoutGroup;
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private readonly StripeBack _lockStripe;
private LoadoutWindow? _loadoutWindow;
private RoleLoadout? _loadout;
/// <summary>
/// Raised if a loadout has been updated.
/// </summary>
public event Action<RoleLoadout>? LoadoutUpdated;
protected RequirementsSelector(T proto, ButtonGroup loadoutGroup)
{
_loadoutGroup = loadoutGroup;
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft,
HorizontalExpand = true,
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
var requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
HasMargins = false,
MouseFilter = MouseFilterMode.Stop,
Children =
{
requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup(RoleLoadout? loadout, (string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
_loadout = loadout;
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label()
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
if (icon != null)
AddChild(icon);
AddChild(titleLabel);
AddChild(Options);
AddChild(_lockStripe);
var loadoutWindowBtn = new Button()
{
Text = Loc.GetString("loadout-window"),
HorizontalAlignment = HAlignment.Right,
Group = _loadoutGroup,
Margin = new Thickness(3f, 0f, 0f, 0f),
};
var collection = IoCManager.Instance!;
var protoManager = collection.Resolve<IPrototypeManager>();
// If no loadout found then disabled button
if (!protoManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(Proto.ID)))
{
loadoutWindowBtn.Disabled = true;
}
// else
else
{
var session = collection.Resolve<IPlayerManager>().LocalSession!;
// TODO: Most of lobby state should be a uicontroller
// trying to handle all this shit is a big-ass mess.
// Every time I touch it I try to make it slightly better but it needs a howitzer dropped on it.
loadoutWindowBtn.OnPressed += args =>
{
if (args.Button.Pressed)
{
// We only create a loadout when necessary to avoid unnecessary DB entries.
_loadout ??= new RoleLoadout(LoadoutSystem.GetJobPrototype(Proto.ID));
_loadout.SetDefault(protoManager);
_loadoutWindow = new LoadoutWindow(_loadout, protoManager.Index(_loadout.Role), session, collection)
{
Title = Loc.GetString(Proto.ID + "-loadout"),
};
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
// If it's a job preview then refresh it.
if (Proto is JobPrototype jobProto)
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(jobProto);
}
_loadoutWindow.OnLoadoutUnpressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.RemoveLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.ReloadProfile();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OnLoadoutPressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.AddLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.ReloadProfile();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OpenCenteredLeft();
_loadoutWindow.OnClose += () =>
{
loadoutWindowBtn.Pressed = false;
_loadoutWindow?.Dispose();
_loadoutWindow = null;
};
}
else
{
CloseLoadout();
}
};
}
AddChild(loadoutWindowBtn);
}
public void CloseLoadout()
{
_loadoutWindow?.Close();
_loadoutWindow?.Dispose();
_loadoutWindow = null;
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90,
HorizontalExpand = true,
};
}
}

View File

@ -19,7 +19,7 @@ public sealed partial class MindTests
await using var pair = await PoolManager.GetServerClient(settings);
// Client is connected with a valid entity & mind
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
// Delete **everything**
@ -28,6 +28,12 @@ public sealed partial class MindTests
await pair.RunTicksSync(5);
Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
foreach (var ent in pair.Client.EntMan.GetEntities())
{
Console.WriteLine(pair.Client.EntMan.ToPrettyString(ent));
}
Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
// Create a new map.
@ -36,7 +42,7 @@ public sealed partial class MindTests
await pair.RunTicksSync(5);
// Client is not attached to anything
Assert.That(pair.Client.Player?.ControlledEntity, Is.Null);
Assert.That(pair.Client.AttachedEntity, Is.Null);
Assert.That(pair.PlayerData?.Mind, Is.Null);
// Attempt to ghost
@ -45,9 +51,9 @@ public sealed partial class MindTests
await pair.RunTicksSync(10);
// Client should be attached to a ghost placed on the new map.
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value);
Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
await pair.CleanReturnAsync();

View File

@ -0,0 +1,44 @@
using Content.Server.Station.Systems;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Preferences;
[TestFixture]
[Ignore("HumanoidAppearance crashes upon loading default profiles.")]
public sealed class LoadoutTests
{
/// <summary>
/// Checks that an empty loadout still spawns with default gear and not naked.
/// </summary>
[Test]
public async Task TestEmptyLoadout()
{
var pair = await PoolManager.GetServerClient(new PoolSettings()
{
Dirty = true,
});
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
// Check that an empty role loadout spawns gear
var stationSystem = entManager.System<StationSpawningSystem>();
var testMap = await pair.CreateTestMap();
// That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing.
var profile = new HumanoidCharacterProfile();
profile.SetLoadout(new RoleLoadout("TestRoleLoadout"));
stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
{
// Sue me, there's so much involved in setting up jobs
Prototype = "CargoTechnician"
}, profile, station: null);
await pair.CleanReturnAsync();
}
}

View File

@ -4,6 +4,8 @@ using Content.Server.Database;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
@ -53,8 +55,6 @@ namespace Content.IntegrationTests.Tests.Preferences
Color.Beige,
new ()
),
ClothingPreference.Jumpskirt,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@ -62,7 +62,8 @@ namespace Content.IntegrationTests.Tests.Preferences
},
PreferenceUnavailableMode.StayInLobby,
new List<string> (),
new List<string>()
new List<string>(),
new Dictionary<string, RoleLoadout>()
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ClothingRemoval : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backpack",
table: "profile");
migrationBuilder.DropColumn(
name: "clothing",
table: "profile");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "backpack",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "clothing",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,103 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class Loadouts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "profile_role_loadout",
columns: table => new
{
profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_id = table.Column<int>(type: "integer", nullable: false),
role_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
table.ForeignKey(
name: "FK_profile_role_loadout_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout_group",
columns: table => new
{
profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false),
group_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
table.ForeignKey(
name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loa~",
column: x => x.profile_role_loadout_id,
principalTable: "profile_role_loadout",
principalColumn: "profile_role_loadout_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout",
columns: table => new
{
profile_loadout_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false),
loadout_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
table.ForeignKey(
name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group~",
column: x => x.profile_loadout_group_id,
principalTable: "profile_loadout_group",
principalColumn: "profile_loadout_group_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_profile_loadout_group_id",
table: "profile_loadout",
column: "profile_loadout_group_id");
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_group_profile_role_loadout_id",
table: "profile_loadout_group",
column: "profile_role_loadout_id");
migrationBuilder.CreateIndex(
name: "IX_profile_role_loadout_profile_id",
table: "profile_role_loadout",
column: "profile_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "profile_loadout");
migrationBuilder.DropTable(
name: "profile_loadout_group");
migrationBuilder.DropTable(
name: "profile_role_loadout");
}
}
}

View File

@ -735,21 +735,11 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer")
.HasColumnName("age");
b.Property<string>("Backpack")
.IsRequired()
.HasColumnType("text")
.HasColumnName("backpack");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("char_name");
b.Property<string>("Clothing")
.IsRequired()
.HasColumnType("text")
.HasColumnName("clothing");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnType("text")
@ -832,6 +822,84 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_loadout_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("LoadoutName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("loadout_name");
b.Property<int>("ProfileLoadoutGroupId")
.HasColumnType("integer")
.HasColumnName("profile_loadout_group_id");
b.HasKey("Id")
.HasName("PK_profile_loadout");
b.HasIndex("ProfileLoadoutGroupId");
b.ToTable("profile_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_loadout_group_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("GroupName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("group_name");
b.Property<int>("ProfileRoleLoadoutId")
.HasColumnType("integer")
.HasColumnName("profile_role_loadout_id");
b.HasKey("Id")
.HasName("PK_profile_loadout_group");
b.HasIndex("ProfileRoleLoadoutId");
b.ToTable("profile_loadout_group", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_role_loadout_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ProfileId")
.HasColumnType("integer")
.HasColumnName("profile_id");
b.Property<string>("RoleName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("role_name");
b.HasKey("Id")
.HasName("PK_profile_role_loadout");
b.HasIndex("ProfileId");
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@ -1519,6 +1587,42 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
.WithMany("Loadouts")
.HasForeignKey("ProfileLoadoutGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
b.Navigation("ProfileLoadoutGroup");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
.WithMany("Groups")
.HasForeignKey("ProfileRoleLoadoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
b.Navigation("ProfileRoleLoadout");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Loadouts")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_role_loadout_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@ -1731,9 +1835,21 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Jobs");
b.Navigation("Loadouts");
b.Navigation("Traits");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Navigation("Loadouts");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ClothingRemoval : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backpack",
table: "profile");
migrationBuilder.DropColumn(
name: "clothing",
table: "profile");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "backpack",
table: "profile",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "clothing",
table: "profile",
type: "TEXT",
nullable: false,
defaultValue: "");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class Loadouts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "profile_role_loadout",
columns: table => new
{
profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_id = table.Column<int>(type: "INTEGER", nullable: false),
role_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
table.ForeignKey(
name: "FK_profile_role_loadout_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout_group",
columns: table => new
{
profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false),
group_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
table.ForeignKey(
name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id",
column: x => x.profile_role_loadout_id,
principalTable: "profile_role_loadout",
principalColumn: "profile_role_loadout_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout",
columns: table => new
{
profile_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false),
loadout_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
table.ForeignKey(
name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group_id",
column: x => x.profile_loadout_group_id,
principalTable: "profile_loadout_group",
principalColumn: "profile_loadout_group_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_profile_loadout_group_id",
table: "profile_loadout",
column: "profile_loadout_group_id");
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_group_profile_role_loadout_id",
table: "profile_loadout_group",
column: "profile_role_loadout_id");
migrationBuilder.CreateIndex(
name: "IX_profile_role_loadout_profile_id",
table: "profile_role_loadout",
column: "profile_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "profile_loadout");
migrationBuilder.DropTable(
name: "profile_loadout_group");
migrationBuilder.DropTable(
name: "profile_role_loadout");
}
}
}

View File

@ -688,21 +688,11 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("age");
b.Property<string>("Backpack")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("backpack");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("char_name");
b.Property<string>("Clothing")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("clothing");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnType("TEXT")
@ -785,6 +775,78 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_id");
b.Property<string>("LoadoutName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("loadout_name");
b.Property<int>("ProfileLoadoutGroupId")
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_group_id");
b.HasKey("Id")
.HasName("PK_profile_loadout");
b.HasIndex("ProfileLoadoutGroupId");
b.ToTable("profile_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_group_id");
b.Property<string>("GroupName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("group_name");
b.Property<int>("ProfileRoleLoadoutId")
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.HasKey("Id")
.HasName("PK_profile_loadout_group");
b.HasIndex("ProfileRoleLoadoutId");
b.ToTable("profile_loadout_group", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER")
.HasColumnName("profile_id");
b.Property<string>("RoleName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("role_name");
b.HasKey("Id")
.HasName("PK_profile_role_loadout");
b.HasIndex("ProfileId");
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@ -1450,6 +1512,42 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
.WithMany("Loadouts")
.HasForeignKey("ProfileLoadoutGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
b.Navigation("ProfileLoadoutGroup");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
.WithMany("Groups")
.HasForeignKey("ProfileRoleLoadoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
b.Navigation("ProfileRoleLoadout");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Loadouts")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_role_loadout_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@ -1662,9 +1760,21 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Jobs");
b.Navigation("Loadouts");
b.Navigation("Traits");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Navigation("Loadouts");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");

View File

@ -56,8 +56,26 @@ namespace Content.Server.Database
.IsUnique();
modelBuilder.Entity<Trait>()
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
.IsUnique();
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
.IsUnique();
modelBuilder.Entity<ProfileRoleLoadout>()
.HasOne(e => e.Profile)
.WithMany(e => e.Loadouts)
.HasForeignKey(e => e.ProfileId)
.IsRequired();
modelBuilder.Entity<ProfileLoadoutGroup>()
.HasOne(e => e.ProfileRoleLoadout)
.WithMany(e => e.Groups)
.HasForeignKey(e => e.ProfileRoleLoadoutId)
.IsRequired();
modelBuilder.Entity<ProfileLoadout>()
.HasOne(e => e.ProfileLoadoutGroup)
.WithMany(e => e.Loadouts)
.HasForeignKey(e => e.ProfileLoadoutGroupId)
.IsRequired();
modelBuilder.Entity<Job>()
.HasIndex(j => j.ProfileId);
@ -337,13 +355,13 @@ namespace Content.Server.Database
public string FacialHairColor { get; set; } = null!;
public string EyeColor { get; set; } = null!;
public string SkinColor { get; set; } = null!;
public string Clothing { get; set; } = null!;
public string Backpack { get; set; } = null!;
public int SpawnPriority { get; set; } = 0;
public List<Job> Jobs { get; } = new();
public List<Antag> Antags { get; } = new();
public List<Trait> Traits { get; } = new();
public List<ProfileRoleLoadout> Loadouts { get; } = new();
[Column("pref_unavailable")] public DbPreferenceUnavailableMode PreferenceUnavailable { get; set; }
public int PreferenceId { get; set; }
@ -387,6 +405,79 @@ namespace Content.Server.Database
public string TraitName { get; set; } = null!;
}
#region Loadouts
/// <summary>
/// Corresponds to a single role's loadout inside the DB.
/// </summary>
public class ProfileRoleLoadout
{
public int Id { get; set; }
public int ProfileId { get; set; }
public Profile Profile { get; set; } = null!;
/// <summary>
/// The corresponding role prototype on the profile.
/// </summary>
public string RoleName { get; set; } = string.Empty;
/// <summary>
/// Store the saved loadout groups. These may get validated and removed when loaded at runtime.
/// </summary>
public List<ProfileLoadoutGroup> Groups { get; set; } = new();
}
/// <summary>
/// Corresponds to a loadout group prototype with the specified loadouts attached.
/// </summary>
public class ProfileLoadoutGroup
{
public int Id { get; set; }
public int ProfileRoleLoadoutId { get; set; }
/// <summary>
/// The corresponding RoleLoadout that owns this.
/// </summary>
public ProfileRoleLoadout ProfileRoleLoadout { get; set; } = null!;
/// <summary>
/// The corresponding group prototype.
/// </summary>
public string GroupName { get; set; } = string.Empty;
/// <summary>
/// Selected loadout prototype. Null if none is set.
/// May get validated at runtime and updated to to the default.
/// </summary>
public List<ProfileLoadout> Loadouts { get; set; } = new();
}
/// <summary>
/// Corresponds to a selected loadout.
/// </summary>
public class ProfileLoadout
{
public int Id { get; set; }
public int ProfileLoadoutGroupId { get; set; }
public ProfileLoadoutGroup ProfileLoadoutGroup { get; set; } = null!;
/// <summary>
/// Corresponding loadout prototype.
/// </summary>
public string LoadoutName { get; set; } = string.Empty;
/*
* Insert extra data here like custom descriptions or colors or whatever.
*/
}
#endregion
public enum DbPreferenceUnavailableMode
{
// These enum values HAVE to match the ones in PreferenceUnavailableMode in Shared.

View File

@ -97,7 +97,7 @@ namespace Content.Server.Administration.Commands
foreach (var slot in slots)
{
invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = startingGear.GetGear(slot.Name, profile);
var gearStr = startingGear.GetGear(slot.Name);
if (gearStr == string.Empty)
{
continue;

View File

@ -13,6 +13,8 @@ using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums;
using Robust.Shared.Network;
@ -40,6 +42,10 @@ namespace Content.Server.Database
.Include(p => p.Profiles).ThenInclude(h => h.Jobs)
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
.Include(p => p.Profiles).ThenInclude(h => h.Traits)
.Include(p => p.Profiles)
.ThenInclude(h => h.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSingleQuery()
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
@ -88,6 +94,9 @@ namespace Content.Server.Database
.Include(p => p.Jobs)
.Include(p => p.Antags)
.Include(p => p.Traits)
.Include(p => p.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSplitQuery()
.SingleOrDefault(h => h.Slot == slot);
@ -179,14 +188,6 @@ namespace Content.Server.Database
if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
sex = sexVal;
var clothing = ClothingPreference.Jumpsuit;
if (Enum.TryParse<ClothingPreference>(profile.Clothing, true, out var clothingVal))
clothing = clothingVal;
var backpack = BackpackPreference.Backpack;
if (Enum.TryParse<BackpackPreference>(profile.Backpack, true, out var backpackVal))
backpack = backpackVal;
var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
@ -209,6 +210,27 @@ namespace Content.Server.Database
}
}
var loadouts = new Dictionary<string, RoleLoadout>();
foreach (var role in profile.Loadouts)
{
var loadout = new RoleLoadout(role.RoleName);
foreach (var group in role.Groups)
{
var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
foreach (var profLoadout in group.Loadouts)
{
groupLoadouts.Add(new Loadout()
{
Prototype = profLoadout.LoadoutName,
});
}
}
loadouts[role.RoleName] = loadout;
}
return new HumanoidCharacterProfile(
profile.CharacterName,
profile.FlavorText,
@ -226,13 +248,12 @@ namespace Content.Server.Database
Color.FromHex(profile.SkinColor),
markings
),
clothing,
backpack,
spawnPriority,
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToList(),
traits.ToList()
traits.ToList(),
loadouts
);
}
@ -259,8 +280,6 @@ namespace Content.Server.Database
profile.FacialHairColor = appearance.FacialHairColor.ToHex();
profile.EyeColor = appearance.EyeColor.ToHex();
profile.SkinColor = appearance.SkinColor.ToHex();
profile.Clothing = humanoid.Clothing.ToString();
profile.Backpack = humanoid.Backpack.ToString();
profile.SpawnPriority = (int) humanoid.SpawnPriority;
profile.Markings = markings;
profile.Slot = slot;
@ -285,6 +304,36 @@ namespace Content.Server.Database
.Select(t => new Trait {TraitName = t})
);
profile.Loadouts.Clear();
foreach (var (role, loadouts) in humanoid.Loadouts)
{
var dz = new ProfileRoleLoadout()
{
RoleName = role,
};
foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
{
var profileGroup = new ProfileLoadoutGroup()
{
GroupName = group,
};
foreach (var loadout in groupLoadouts)
{
profileGroup.Loadouts.Add(new ProfileLoadout()
{
LoadoutName = loadout.Prototype,
});
}
dz.Groups.Add(profileGroup);
}
profile.Loadouts.Add(dz);
}
return profile;
}
#endregion

View File

@ -144,8 +144,8 @@ public sealed class ParadoxAnomalySystem : EntitySystem
if (job.StartingGear != null && _proto.TryIndex<StartingGearPrototype>(job.StartingGear, out var gear))
{
_stationSpawning.EquipStartingGear(spawned, gear, profile);
_stationSpawning.EquipIdCard(spawned,
_stationSpawning.EquipStartingGear(spawned, gear);
_stationSpawning.SetPdaAndIdCardData(spawned,
profile.Name,
job,
station);

View File

@ -709,7 +709,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
_humanoid.LoadProfile(mob, profile);
var gear = _prototypeManager.Index(spawnDetails.GearProto);
_stationSpawning.EquipStartingGear(mob, gear, profile);
_stationSpawning.EquipStartingGear(mob, gear);
_npcFaction.RemoveFaction(mob, "NanoTrasen", false);
_npcFaction.AddFaction(mob, "Syndicate");

View File

@ -250,7 +250,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
_mindSystem.TransferTo(newMind, mob);
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
_stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
_stationSpawningSystem.EquipStartingGear(mob, pirateGear);
_npcFaction.RemoveFaction(mob, EnemyFactionId, false);
_npcFaction.AddFaction(mob, PirateFactionId);

View File

@ -22,6 +22,7 @@ using Content.Server.Worldgen.Tools;
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
using Content.Shared.Kitchen;
using Content.Shared.Players.PlayTimeTracking;
namespace Content.Server.IoC
{
@ -58,6 +59,7 @@ namespace Content.Server.IoC
IoCManager.Register<PoissonDiskSampler>();
IoCManager.Register<DiscordWebhook>();
IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
}
}

View File

@ -55,7 +55,7 @@ public delegate void CalcPlayTimeTrackersCallback(ICommonSession player, HashSet
/// Operations like refreshing and sending play time info to clients are deferred until the next frame (note: not tick).
/// </para>
/// </remarks>
public sealed partial class PlayTimeTrackingManager
public sealed partial class PlayTimeTrackingManager : ISharedPlaytimeManager
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IServerNetManager _net = default!;
@ -209,6 +209,11 @@ public sealed partial class PlayTimeTrackingManager
}
}
public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
{
return GetTrackerTimes(session);
}
private void SendPlayTimes(ICommonSession pSession)
{
var roles = GetTrackerTimes(pSession);

View File

@ -8,6 +8,7 @@ using Content.Shared.CCVar;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
@ -25,6 +26,7 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _protos = default!;
// Cache player prefs on the server so we don't need as much async hell related to them.
@ -98,8 +100,10 @@ namespace Content.Server.Preferences.Managers
}
var curPrefs = prefsData.Prefs!;
var session = _playerManager.GetSessionById(userId);
var collection = IoCManager.Instance!;
profile.EnsureValid(_cfg, _protos);
profile.EnsureValid(session, collection);
var profiles = new Dictionary<int, ICharacterProfile>(curPrefs.Characters)
{
@ -260,17 +264,20 @@ namespace Content.Server.Preferences.Managers
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random());
}
return SanitizePreferences(prefs);
var session = _playerManager.GetSessionById(userId);
var collection = IoCManager.Instance!;
return SanitizePreferences(session, prefs, collection);
}
private PlayerPreferences SanitizePreferences(PlayerPreferences prefs)
private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPreferences prefs, IDependencyCollection collection)
{
// Clean up preferences in case of changes to the game,
// such as removed jobs still being selected.
return new PlayerPreferences(prefs.Characters.Select(p =>
{
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(_cfg, _protos));
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(session, collection));
}), prefs.SelectedCharacterIndex, prefs.AdminOOCColor);
}

View File

@ -69,7 +69,7 @@ public sealed class SpawnPointSystem : EntitySystem
// TODO: Refactor gameticker spawning code so we don't have to do this!
var points2 = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
if (points2.MoveNext(out var uid, out var spawnPoint, out var xform))
if (points2.MoveNext(out var spawnPoint, out var xform))
{
possiblePositions.Add(xform.Coordinates);
}

View File

@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Access.Systems;
using Content.Server.DetailExaminable;
using Content.Server.Humanoid;
@ -11,10 +12,12 @@ using Content.Server.Station.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
@ -89,7 +92,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
if (station != null && profile != null)
{
/// Try to call the character's preferred spawner first.
// Try to call the character's preferred spawner first.
if (_spawnerCallbacks.TryGetValue(profile.SpawnPriority, out var preferredSpawner))
{
preferredSpawner(ev);
@ -104,9 +107,11 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
}
else
{
/// Call all of them in the typical order.
// Call all of them in the typical order.
foreach (var typicalSpawner in _spawnerCallbacks.Values)
{
typicalSpawner(ev);
}
}
}
@ -137,7 +142,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
EntityUid? station,
EntityUid? entity = null)
{
_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out JobPrototype? prototype);
_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var prototype);
// If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff.
if (prototype?.JobEntity != null)
@ -179,13 +184,52 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
if (prototype?.StartingGear != null)
{
var startingGear = _prototypeManager.Index<StartingGearPrototype>(prototype.StartingGear);
EquipStartingGear(entity.Value, startingGear, profile);
if (profile != null)
EquipIdCard(entity.Value, profile.Name, prototype, station);
EquipStartingGear(entity.Value, startingGear);
}
// Run loadouts after so stuff like storage loadouts can get
var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto))
{
RoleLoadout? loadout = null;
profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
// Set to default if not present
if (loadout == null)
{
loadout = new RoleLoadout(jobLoadout);
loadout.SetDefault(_prototypeManager);
}
// Order loadout selections by the order they appear on the prototype.
foreach (var group in loadout.SelectedLoadouts.OrderBy(x => roleProto.Groups.FindIndex(e => e == x.Key)))
{
foreach (var items in group.Value)
{
if (!_prototypeManager.TryIndex(items.Prototype, out var loadoutProto))
{
Log.Error($"Unable to find loadout prototype for {items.Prototype}");
continue;
}
if (!_prototypeManager.TryIndex(loadoutProto.Equipment, out var startingGear))
{
Log.Error($"Unable to find starting gear {loadoutProto.Equipment} for loadout {loadoutProto}");
continue;
}
// Handle any extra data here.
EquipStartingGear(entity.Value, startingGear);
}
}
}
if (profile != null)
{
if (prototype != null)
SetPdaAndIdCardData(entity.Value, profile.Name, prototype, station);
_humanoidSystem.LoadProfile(entity.Value, profile);
_metaSystem.SetEntityName(entity.Value, profile.Name);
if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText))
@ -211,13 +255,13 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
}
/// <summary>
/// Equips an ID card and PDA onto the given entity.
/// Sets the ID card and PDA name, job, and access data.
/// </summary>
/// <param name="entity">Entity to load out.</param>
/// <param name="characterName">Character name to use for the ID.</param>
/// <param name="jobPrototype">Job prototype to use for the PDA and ID.</param>
/// <param name="station">The station this player is being spawned on.</param>
public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station)
public void SetPdaAndIdCardData(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station)
{
if (!InventorySystem.TryGetSlotEntity(entity, "id", out var idUid))
return;

View File

@ -1,4 +1,6 @@
using System.Linq;
using Content.Shared.Clothing.Components;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Prototypes;
@ -24,12 +26,94 @@ public sealed class LoadoutSystem : EntitySystem
SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit);
}
public static string GetJobPrototype(string? loadout)
{
if (string.IsNullOrEmpty(loadout))
return string.Empty;
return "Job" + loadout;
}
/// <summary>
/// Tries to get the first entity prototype for operations such as sprite drawing.
/// </summary>
public EntProtoId? GetFirstOrNull(LoadoutPrototype loadout)
{
if (!_protoMan.TryIndex(loadout.Equipment, out var gear))
return null;
var count = gear.Equipment.Count + gear.Inhand.Count + gear.Storage.Values.Sum(x => x.Count);
if (count == 1)
{
if (gear.Equipment.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Equipment.Values.First(), out var proto))
{
return proto.ID;
}
if (gear.Inhand.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Inhand[0], out proto))
{
return proto.ID;
}
// Storage moment
foreach (var ents in gear.Storage.Values)
{
foreach (var ent in ents)
{
return ent;
}
}
}
return null;
}
/// <summary>
/// Tries to get the name of a loadout.
/// </summary>
public string GetName(LoadoutPrototype loadout)
{
if (!_protoMan.TryIndex(loadout.Equipment, out var gear))
return Loc.GetString("loadout-unknown");
var count = gear.Equipment.Count + gear.Storage.Values.Sum(o => o.Count) + gear.Inhand.Count;
if (count == 1)
{
if (gear.Equipment.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Equipment.Values.First(), out var proto))
{
return proto.Name;
}
if (gear.Inhand.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Inhand[0], out proto))
{
return proto.Name;
}
foreach (var values in gear.Storage.Values)
{
if (values.Count != 1)
continue;
if (_protoMan.TryIndex<EntityPrototype>(values[0], out proto))
{
return proto.Name;
}
break;
}
}
return Loc.GetString($"loadout-{loadout.ID}");
}
private void OnMapInit(EntityUid uid, LoadoutComponent component, MapInitEvent args)
{
if (component.Prototypes == null)
return;
var proto = _protoMan.Index<StartingGearPrototype>(_random.Pick(component.Prototypes));
_station.EquipStartingGear(uid, proto, null);
_station.EquipStartingGear(uid, proto);
}
}

View File

@ -29,6 +29,10 @@
<ItemGroup>
<Folder Include="DeltaV\Abilities\" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Preferences\BackpackPreference.cs" />
<Compile Remove="Preferences\ClothingPreference.cs" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
</Project>

View File

@ -66,14 +66,14 @@ public sealed partial class SpeciesPrototype : IPrototype
/// <summary>
/// Humanoid species variant used by this entity.
/// </summary>
[DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype { get; private set; } = default!;
[DataField(required: true)]
public EntProtoId Prototype { get; private set; } = default!;
/// <summary>
/// Prototype used by the species for the dress-up doll in various menus.
/// </summary>
[DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string DollPrototype { get; private set; } = default!;
[DataField(required: true)]
public EntProtoId DollPrototype { get; private set; } = default!;
/// <summary>
/// Method of skin coloration used by the species.
@ -120,12 +120,6 @@ public sealed partial class SpeciesPrototype : IPrototype
/// </summary>
[DataField]
public int MaxAge = 120;
/// <summary>
/// The Style used for the guidebook info link in the character profile editor
/// </summary>
[DataField]
public string GuideBookIcon = "SpeciesInfoDefault";
}
public enum SpeciesNaming : byte

View File

@ -0,0 +1,12 @@
using Robust.Shared.Player;
namespace Content.Shared.Players.PlayTimeTracking;
public interface ISharedPlaytimeManager
{
/// <summary>
/// Gets the playtimes for the session or an empty dictionary if none found.
/// </summary>
IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session);
}

View File

@ -1,12 +0,0 @@
namespace Content.Shared.Preferences
{
/// <summary>
/// The backpack preference for a profile. Stored in database!
/// </summary>
public enum BackpackPreference
{
Backpack,
Satchel,
Duffelbag
}
}

View File

@ -1,11 +0,0 @@
namespace Content.Shared.Preferences
{
/// <summary>
/// The clothing preference for a profile. Stored in database!
/// </summary>
public enum ClothingPreference
{
Jumpsuit,
Jumpskirt
}
}

View File

@ -1,15 +1,17 @@
using System.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Random.Helpers;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.Traits;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
@ -43,6 +45,11 @@ namespace Content.Shared.Preferences
private readonly List<string> _antagPreferences;
private readonly List<string> _traitPreferences;
public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
private Dictionary<string, RoleLoadout> _loadouts;
// What in the lord is happening here.
private HumanoidCharacterProfile(
string name,
string flavortext,
@ -51,13 +58,12 @@ namespace Content.Shared.Preferences
Sex sex,
Gender gender,
HumanoidCharacterAppearance appearance,
ClothingPreference clothing,
BackpackPreference backpack,
SpawnPriorityPreference spawnPriority,
Dictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
List<string> antagPreferences,
List<string> traitPreferences)
List<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
{
Name = name;
FlavorText = flavortext;
@ -66,13 +72,12 @@ namespace Content.Shared.Preferences
Sex = sex;
Gender = gender;
Appearance = appearance;
Clothing = clothing;
Backpack = backpack;
SpawnPriority = spawnPriority;
_jobPriorities = jobPriorities;
PreferenceUnavailable = preferenceUnavailable;
_antagPreferences = antagPreferences;
_traitPreferences = traitPreferences;
_loadouts = loadouts;
}
/// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
@ -80,15 +85,16 @@ namespace Content.Shared.Preferences
HumanoidCharacterProfile other,
Dictionary<string, JobPriority> jobPriorities,
List<string> antagPreferences,
List<string> traitPreferences)
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack, other.SpawnPriority,
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences)
List<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.SpawnPriority,
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, loadouts)
{
}
/// <summary>Copy constructor</summary>
private HumanoidCharacterProfile(HumanoidCharacterProfile other)
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences))
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts))
{
}
@ -100,15 +106,14 @@ namespace Content.Shared.Preferences
Sex sex,
Gender gender,
HumanoidCharacterAppearance appearance,
ClothingPreference clothing,
BackpackPreference backpack,
SpawnPriorityPreference spawnPriority,
IReadOnlyDictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
IReadOnlyList<string> antagPreferences,
IReadOnlyList<string> traitPreferences)
: this(name, flavortext, species, age, sex, gender, appearance, clothing, backpack, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences))
IReadOnlyList<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(name, flavortext, species, age, sex, gender, appearance, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences), new Dictionary<string, RoleLoadout>(loadouts))
{
}
@ -125,8 +130,6 @@ namespace Content.Shared.Preferences
Sex.Male,
Gender.Male,
new HumanoidCharacterAppearance(),
ClothingPreference.Jumpsuit,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@ -134,7 +137,8 @@ namespace Content.Shared.Preferences
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>())
new List<string>(),
new Dictionary<string, RoleLoadout>())
{
}
@ -153,8 +157,6 @@ namespace Content.Shared.Preferences
Sex.Male,
Gender.Male,
HumanoidCharacterAppearance.DefaultWithSpecies(species),
ClothingPreference.Jumpsuit,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@ -162,7 +164,8 @@ namespace Content.Shared.Preferences
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>());
new List<string>(),
new Dictionary<string, RoleLoadout>());
}
// TODO: This should eventually not be a visual change only.
@ -207,11 +210,11 @@ namespace Content.Shared.Preferences
var name = GetName(species, gender);
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, SpawnPriorityPreference.None,
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High},
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>());
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>());
}
public string Name { get; private set; }
@ -231,8 +234,6 @@ namespace Content.Shared.Preferences
[DataField("appearance")]
public HumanoidCharacterAppearance Appearance { get; private set; }
public ClothingPreference Clothing { get; private set; }
public BackpackPreference Backpack { get; private set; }
public SpawnPriorityPreference SpawnPriority { get; private set; }
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
public IReadOnlyList<string> AntagPreferences => _antagPreferences;
@ -275,21 +276,14 @@ namespace Content.Shared.Preferences
return new(this) { Appearance = appearance };
}
public HumanoidCharacterProfile WithClothingPreference(ClothingPreference clothing)
{
return new(this) { Clothing = clothing };
}
public HumanoidCharacterProfile WithBackpackPreference(BackpackPreference backpack)
{
return new(this) { Backpack = backpack };
}
public HumanoidCharacterProfile WithSpawnPriorityPreference(SpawnPriorityPreference spawnPriority)
{
return new(this) { SpawnPriority = spawnPriority };
}
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
{
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences);
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
@ -303,7 +297,7 @@ namespace Content.Shared.Preferences
{
dictionary[jobId] = priority;
}
return new(this, dictionary, _antagPreferences, _traitPreferences);
return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
@ -313,7 +307,7 @@ namespace Content.Shared.Preferences
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
{
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences);
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
@ -333,7 +327,8 @@ namespace Content.Shared.Preferences
list.Remove(antagId);
}
}
return new(this, _jobPriorities, list, _traitPreferences);
return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
@ -355,7 +350,7 @@ namespace Content.Shared.Preferences
list.Remove(traitId);
}
}
return new(this, _jobPriorities, _antagPreferences, list);
return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
}
public string Summary =>
@ -375,17 +370,19 @@ namespace Content.Shared.Preferences
if (Gender != other.Gender) return false;
if (Species != other.Species) return false;
if (PreferenceUnavailable != other.PreferenceUnavailable) return false;
if (Clothing != other.Clothing) return false;
if (Backpack != other.Backpack) return false;
if (SpawnPriority != other.SpawnPriority) return false;
if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false;
if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false;
if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
return Appearance.MemberwiseEquals(other.Appearance);
}
public void EnsureValid(IConfigurationManager configManager, IPrototypeManager prototypeManager)
public void EnsureValid(ICommonSession session, IDependencyCollection collection)
{
var configManager = collection.Resolve<IConfigurationManager>();
var prototypeManager = collection.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex<SpeciesPrototype>(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
{
Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
@ -466,21 +463,6 @@ namespace Content.Shared.Preferences
_ => PreferenceUnavailableMode.StayInLobby // Invalid enum values.
};
var clothing = Clothing switch
{
ClothingPreference.Jumpsuit => ClothingPreference.Jumpsuit,
ClothingPreference.Jumpskirt => ClothingPreference.Jumpskirt,
_ => ClothingPreference.Jumpsuit // Invalid enum values.
};
var backpack = Backpack switch
{
BackpackPreference.Backpack => BackpackPreference.Backpack,
BackpackPreference.Satchel => BackpackPreference.Satchel,
BackpackPreference.Duffelbag => BackpackPreference.Duffelbag,
_ => BackpackPreference.Backpack // Invalid enum values.
};
var spawnPriority = SpawnPriority switch
{
SpawnPriorityPreference.None => SpawnPriorityPreference.None,
@ -513,8 +495,6 @@ namespace Content.Shared.Preferences
Sex = sex;
Gender = gender;
Appearance = appearance;
Clothing = clothing;
Backpack = backpack;
SpawnPriority = spawnPriority;
_jobPriorities.Clear();
@ -531,12 +511,31 @@ namespace Content.Shared.Preferences
_traitPreferences.Clear();
_traitPreferences.AddRange(traits);
// Checks prototypes exist for all loadouts and dump / set to default if not.
var toRemove = new ValueList<string>();
foreach (var (roleName, loadouts) in _loadouts)
{
if (!prototypeManager.HasIndex<RoleLoadoutPrototype>(roleName))
{
toRemove.Add(roleName);
continue;
}
loadouts.EnsureValid(session, collection);
}
foreach (var value in toRemove)
{
_loadouts.Remove(value);
}
}
public ICharacterProfile Validated(IConfigurationManager configManager, IPrototypeManager prototypeManager)
public ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection)
{
var profile = new HumanoidCharacterProfile(this);
profile.EnsureValid(configManager, prototypeManager);
profile.EnsureValid(session, collection);
return profile;
}
@ -562,16 +561,49 @@ namespace Content.Shared.Preferences
Age,
Sex,
Gender,
Appearance,
Clothing,
Backpack
Appearance
),
SpawnPriority,
PreferenceUnavailable,
_jobPriorities,
_antagPreferences,
_traitPreferences
_traitPreferences,
_loadouts
);
}
public void SetLoadout(RoleLoadout loadout)
{
_loadouts[loadout.Role.Id] = loadout;
}
public HumanoidCharacterProfile WithLoadout(RoleLoadout loadout)
{
// Deep copies so we don't modify the DB profile.
var copied = new Dictionary<string, RoleLoadout>();
foreach (var proto in _loadouts)
{
if (proto.Key == loadout.Role)
continue;
copied[proto.Key] = proto.Value.Clone();
}
copied[loadout.Role] = loadout.Clone();
return new(this, _jobPriorities, _antagPreferences, _traitPreferences, copied);
}
public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
{
if (!_loadouts.TryGetValue(id, out var loadout))
{
loadout = new RoleLoadout(id);
loadout.SetDefault(protoManager, force: true);
}
loadout.SetDefault(protoManager);
return loadout;
}
}
}

View File

@ -1,5 +1,6 @@
using Content.Shared.Humanoid;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences
@ -15,11 +16,11 @@ namespace Content.Shared.Preferences
/// <summary>
/// Makes this profile valid so there's no bad data like negative ages.
/// </summary>
void EnsureValid(IConfigurationManager configManager, IPrototypeManager prototypeManager);
void EnsureValid(ICommonSession session, IDependencyCollection collection);
/// <summary>
/// Gets a copy of this profile that has <see cref="EnsureValid"/> applied, i.e. no invalid data.
/// </summary>
ICharacterProfile Validated(IConfigurationManager configManager, IPrototypeManager prototypeManager);
ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection);
}
}

View File

@ -0,0 +1,29 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
/// <summary>
/// Uses a <see cref="LoadoutEffectGroupPrototype"/> prototype as a singular effect that can be re-used.
/// </summary>
public sealed partial class GroupLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
public ProtoId<LoadoutEffectGroupPrototype> Proto;
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
foreach (var effect in effectsProto.Effects)
{
if (!effect.Validate(loadout, session, collection, out reason))
return false;
}
reason = null;
return true;
}
}

View File

@ -0,0 +1,30 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
/// <summary>
/// Checks for a job requirement to be met such as playtime.
/// </summary>
public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
public JobRequirement Requirement = default!;
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
var manager = collection.Resolve<ISharedPlaytimeManager>();
var playtimes = manager.GetPlayTimes(session);
var isWhitelisted = session.ContentData()?.Whitelisted ?? false; // DeltaV - Whitelist requirement
return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason,
collection.Resolve<IEntityManager>(),
collection.Resolve<IPrototypeManager>(),
isWhitelisted); // DeltaV
}
}

View File

@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
[ImplicitDataDefinitionForInheritors]
public abstract partial class LoadoutEffect
{
/// <summary>
/// Tries to validate the effect.
/// </summary>
public abstract bool Validate(
RoleLoadout loadout,
ICommonSession session,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason);
public virtual void Apply(RoleLoadout loadout) {}
}

View File

@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts.Effects;
/// <summary>
/// Stores a group of loadout effects in a prototype for re-use.
/// </summary>
[Prototype]
public sealed partial class LoadoutEffectGroupPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
[DataField(required: true)]
public List<LoadoutEffect> Effects = new();
}

View File

@ -0,0 +1,40 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
public int Cost = 1;
public override bool Validate(
RoleLoadout loadout,
ICommonSession session,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
var protoManager = collection.Resolve<IPrototypeManager>();
if (!protoManager.TryIndex(loadout.Role, out var roleProto) || roleProto.Points == null)
{
return true;
}
if (loadout.Points <= Cost)
{
reason = FormattedMessage.FromUnformatted("loadout-group-points-insufficient");
return false;
}
return true;
}
public override void Apply(RoleLoadout loadout)
{
loadout.Points -= Cost;
}
}

View File

@ -0,0 +1,13 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Specifies the selected prototype and custom data for a loadout.
/// </summary>
[Serializable, NetSerializable]
public sealed class Loadout
{
public ProtoId<LoadoutPrototype> Prototype;
}

View File

@ -0,0 +1,34 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Corresponds to a set of loadouts for a particular slot.
/// </summary>
[Prototype("loadoutGroup")]
public sealed partial class LoadoutGroupPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
/// <summary>
/// User-friendly name for the group.
/// </summary>
[DataField(required: true)]
public LocId Name;
/// <summary>
/// Minimum number of loadouts that need to be specified for this category.
/// </summary>
[DataField]
public int MinLimit = 1;
/// <summary>
/// Maximum limit for the category.
/// </summary>
[DataField]
public int MaxLimit = 1;
[DataField(required: true)]
public List<ProtoId<LoadoutPrototype>> Loadouts = new();
}

View File

@ -0,0 +1,25 @@
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Individual loadout item to be applied.
/// </summary>
[Prototype]
public sealed partial class LoadoutPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
[DataField(required: true)]
public ProtoId<StartingGearPrototype> Equipment;
/// <summary>
/// Effects to be applied when the loadout is applied.
/// These can also return true or false for validation purposes.
/// </summary>
[DataField]
public List<LoadoutEffect> Effects = new();
}

View File

@ -0,0 +1,260 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Random;
using Robust.Shared.Collections;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Contains all of the selected data for a role's loadout.
/// </summary>
[Serializable, NetSerializable]
public sealed class RoleLoadout
{
public readonly ProtoId<RoleLoadoutPrototype> Role;
public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
/*
* Loadout-specific data used for validation.
*/
public int? Points;
public RoleLoadout(ProtoId<RoleLoadoutPrototype> role)
{
Role = role;
}
public RoleLoadout Clone()
{
var weh = new RoleLoadout(Role);
foreach (var selected in SelectedLoadouts)
{
weh.SelectedLoadouts.Add(selected.Key, new List<Loadout>(selected.Value));
}
return weh;
}
/// <summary>
/// Ensures all prototypes exist and effects can be applied.
/// </summary>
public void EnsureValid(ICommonSession session, IDependencyCollection collection)
{
var groupRemove = new ValueList<string>();
var protoManager = collection.Resolve<IPrototypeManager>();
if (!protoManager.TryIndex(Role, out var roleProto))
{
SelectedLoadouts.Clear();
return;
}
// Reset points to recalculate.
Points = roleProto.Points;
foreach (var (group, groupLoadouts) in SelectedLoadouts)
{
// Dump if Group doesn't exist
if (!protoManager.TryIndex(group, out var groupProto))
{
groupRemove.Add(group);
continue;
}
var loadouts = groupLoadouts[..Math.Min(groupLoadouts.Count, groupProto.MaxLimit)];
// Validate first
for (var i = loadouts.Count - 1; i >= 0; i--)
{
var loadout = loadouts[i];
if (!protoManager.TryIndex(loadout.Prototype, out var loadoutProto))
{
loadouts.RemoveAt(i);
continue;
}
// Validate the loadout can be applied (e.g. points).
if (!IsValid(session, loadout.Prototype, collection, out _))
{
loadouts.RemoveAt(i);
continue;
}
Apply(loadoutProto);
}
// Apply defaults if required
// Technically it's possible for someone to game themselves into loadouts they shouldn't have
// If you put invalid ones first but that's your fault for not using sensible defaults
if (loadouts.Count < groupProto.MinLimit)
{
for (var i = 0; i < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); i++)
{
if (!protoManager.TryIndex(groupProto.Loadouts[i], out var loadoutProto))
continue;
var defaultLoadout = new Loadout()
{
Prototype = loadoutProto.ID,
};
if (loadouts.Contains(defaultLoadout))
continue;
// Still need to apply the effects even if validation is ignored.
loadouts.Add(defaultLoadout);
Apply(loadoutProto);
}
}
SelectedLoadouts[group] = loadouts;
}
foreach (var value in groupRemove)
{
SelectedLoadouts.Remove(value);
}
}
private void Apply(LoadoutPrototype loadoutProto)
{
foreach (var effect in loadoutProto.Effects)
{
effect.Apply(this);
}
}
/// <summary>
/// Resets the selected loadouts to default if no data is present.
/// </summary>
public void SetDefault(IPrototypeManager protoManager, bool force = false)
{
if (force)
SelectedLoadouts.Clear();
var roleProto = protoManager.Index(Role);
for (var i = roleProto.Groups.Count - 1; i >= 0; i--)
{
var group = roleProto.Groups[i];
if (!protoManager.TryIndex(group, out var groupProto))
continue;
if (SelectedLoadouts.ContainsKey(group))
continue;
SelectedLoadouts[group] = new List<Loadout>();
if (groupProto.MinLimit > 0)
{
// Apply any loadouts we can.
for (var j = 0; j < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); j++)
{
AddLoadout(group, groupProto.Loadouts[j], protoManager);
}
}
}
}
/// <summary>
/// Returns whether a loadout is valid or not.
/// </summary>
public bool IsValid(ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
var protoManager = collection.Resolve<IPrototypeManager>();
if (!protoManager.TryIndex(loadout, out var loadoutProto))
{
// Uhh
reason = FormattedMessage.FromMarkup("");
return false;
}
if (!protoManager.TryIndex(Role, out var roleProto))
{
reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
return false;
}
var valid = true;
foreach (var effect in loadoutProto.Effects)
{
valid = valid && effect.Validate(this, session, collection, out reason);
}
return valid;
}
/// <summary>
/// Applies the specified loadout to this group.
/// </summary>
public bool AddLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
{
var groupLoadouts = SelectedLoadouts[selectedGroup];
// Need to unselect existing ones if we're at or above limit
var limit = Math.Max(0, groupLoadouts.Count + 1 - protoManager.Index(selectedGroup).MaxLimit);
for (var i = 0; i < groupLoadouts.Count; i++)
{
var loadout = groupLoadouts[i];
if (loadout.Prototype != selectedLoadout)
{
// Remove any other loadouts that might push it above the limit.
if (limit > 0)
{
limit--;
groupLoadouts.RemoveAt(i);
i--;
}
continue;
}
DebugTools.Assert(false);
return false;
}
groupLoadouts.Add(new Loadout()
{
Prototype = selectedLoadout,
});
return true;
}
/// <summary>
/// Removed the specified loadout from this group.
/// </summary>
public bool RemoveLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
{
// Although this may bring us below minimum we'll let EnsureValid handle it.
var groupLoadouts = SelectedLoadouts[selectedGroup];
for (var i = 0; i < groupLoadouts.Count; i++)
{
var loadout = groupLoadouts[i];
if (loadout.Prototype != selectedLoadout)
continue;
groupLoadouts.RemoveAt(i);
return true;
}
return false;
}
}

View File

@ -0,0 +1,29 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Corresponds to a Job / Antag prototype and specifies loadouts
/// </summary>
[Prototype]
public sealed partial class RoleLoadoutPrototype : IPrototype
{
/*
* Separate to JobPrototype / AntagPrototype as they are turning into messy god classes.
*/
[IdDataField]
public string ID { get; } = string.Empty;
/// <summary>
/// Groups that comprise this role loadout.
/// </summary>
[DataField(required: true)]
public List<ProtoId<LoadoutGroupPrototype>> Groups = new();
/// <summary>
/// How many points are allotted for this role loadout prototype.
/// </summary>
[DataField]
public int? Points;
}

View File

@ -97,7 +97,7 @@ namespace Content.Shared.Roles
/// </summary>
public static bool TryRequirementMet(
JobRequirement requirement,
Dictionary<string, TimeSpan> playTimes,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager prototypes,
@ -164,7 +164,7 @@ namespace Content.Shared.Roles
return true;
reason = FormattedMessage.FromMarkup(Loc.GetString(
"role-timer-overall-insufficient",
"role-timer-overall-insufficient",
("time", Math.Ceiling(overallDiff))));
return false;
}

View File

@ -10,38 +10,21 @@ namespace Content.Shared.Roles
[DataField]
public Dictionary<string, EntProtoId> Equipment = new();
/// <summary>
/// if empty, there is no skirt override - instead the uniform provided in equipment is added.
/// </summary>
[DataField]
public EntProtoId? InnerClothingSkirt;
[DataField]
public EntProtoId? Satchel;
[DataField]
public EntProtoId? Duffelbag;
[DataField]
public List<EntProtoId> Inhand = new(0);
/// <summary>
/// Inserts entities into the specified slot's storage (if it does have storage).
/// </summary>
[DataField]
public Dictionary<string, List<EntProtoId>> Storage = new();
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = string.Empty;
public string GetGear(string slot, HumanoidCharacterProfile? profile)
public string GetGear(string slot)
{
if (profile != null)
{
if (slot == "jumpsuit" && profile.Clothing == ClothingPreference.Jumpskirt && !string.IsNullOrEmpty(InnerClothingSkirt)
|| slot == "jumpsuit" && profile.Species == "Harpy" && !string.IsNullOrEmpty(InnerClothingSkirt)) //DeltaV adds this line to prevent Harpies from starting with jumpsuits
return InnerClothingSkirt;
if (slot == "back" && profile.Backpack == BackpackPreference.Satchel && !string.IsNullOrEmpty(Satchel))
return Satchel;
if (slot == "back" && profile.Backpack == BackpackPreference.Duffelbag && !string.IsNullOrEmpty(Duffelbag))
return Duffelbag;
}
return Equipment.TryGetValue(slot, out var equipment) ? equipment : string.Empty;
}
}

View File

@ -3,6 +3,9 @@ using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Collections;
namespace Content.Shared.Station;
@ -10,40 +13,69 @@ public abstract class SharedStationSpawningSystem : EntitySystem
{
[Dependency] protected readonly InventorySystem InventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
/// <summary>
/// Equips starting gear onto the given entity.
/// </summary>
/// <param name="entity">Entity to load out.</param>
/// <param name="startingGear">Starting gear to use.</param>
/// <param name="profile">Character profile to use, if any.</param>
public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile)
public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear)
{
if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
{
foreach (var slot in slotDefinitions)
{
var equipmentStr = startingGear.GetGear(slot.Name, profile);
var equipmentStr = startingGear.GetGear(slot.Name);
if (!string.IsNullOrEmpty(equipmentStr))
{
var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true, force:true);
InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, silent: true, force:true);
}
}
}
if (!TryComp(entity, out HandsComponent? handsComponent))
return;
var inhand = startingGear.Inhand;
var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
foreach (var prototype in inhand)
if (TryComp(entity, out HandsComponent? handsComponent))
{
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
var inhand = startingGear.Inhand;
var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
foreach (var prototype in inhand)
{
_handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
{
_handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
}
}
}
if (startingGear.Storage.Count > 0)
{
var coords = _xformSystem.GetMapCoordinates(entity);
var ents = new ValueList<EntityUid>();
TryComp(entity, out InventoryComponent? inventoryComp);
foreach (var (slot, entProtos) in startingGear.Storage)
{
if (entProtos.Count == 0)
continue;
foreach (var ent in entProtos)
{
ents.Add(Spawn(ent, coords));
}
if (inventoryComp != null &&
InventorySystem.TryGetSlotEntity(entity, slot, out var slotEnt, inventoryComponent: inventoryComp) &&
TryComp(slotEnt, out StorageComponent? storage))
{
foreach (var ent in ents)
{
_storage.Insert(slotEnt.Value, ent, out _, storageComp: storage, playSound: false);
}
}
}
}
}

View File

@ -1 +1,2 @@
job-description-medical-borg = Half-human, Half-machine. Follow your laws, serve the crew, and assist the medical department.
job-description-courier = Deliver mail and other packages from and to logistics. Avoid dogs.

View File

@ -1 +1,2 @@
job-name-medical-borg = Medical Cyborg
job-name-courier = Courier

View File

@ -0,0 +1,38 @@
# This file will contain strings for both DeltaV and Nyanotrasen loadouts, because I'm lazy
# Logistics
loadout-group-courier-head = Courier head
loadout-group-courier-jumpsuit = Courier jumpsuit
loadout-group-courier-shoes = Courier shoes
loadout-group-courier-outerclothing = Courier outer clothing
loadout-group-courier-id = Courier ID
loadout-group-mail-carrier-head = Mail Carrier head
loadout-group-mail-carrier-jumpsuit = Mail Carrier jumpsuit
loadout-group-mail-carrier-outerclothing = Mail Carrier outer clothing
# Epistemics
loadout-group-mantis-head = Mantis head
loadout-group-mantis-jumpsuit = Mantis jumpsuit
loadout-group-mantis-backpack = Mantis backpack
loadout-group-mantis-outerclothing = Mantis outer clothing
loadout-group-mantis-shoes = Mantis shoes
loadout-group-mantis-gloves = Mantis gloves
# Security
loadout-group-brig-medic-head = Brigmedic head
loadout-group-brig-medic-jumpsuit = Brigmedic jumpsuit
loadout-group-brig-medic-back = Brigmedic backpack
loadout-group-prison-guard-head = Prison Guard head
loadout-group-prison-guard-jumpsuit = Prison Guard jumpsuit
# Wildcards
loadout-group-prisoner-jumpsuit = Prisoner jumpsuit
loadout-group-martial-artist-jumpsuit = Martial Artist jumpsuit
loadout-group-martial-belt = Martial Artist belt
loadout-group-martial-artist-shoes = Martial Artist shoes
loadout-group-gladiator-jumpsuit = Gladiator jumpsuit
loadout-group-gladiator-outerclothing = Gladiator outer clothing

View File

@ -45,8 +45,4 @@ job-description-security = Catch criminals and enemies of the station, enforce t
job-description-serviceworker = Learn the basics of bartending, cooking, and growing plants.
job-description-visitor = Enjoy your visit to the station.
job-description-warden = Patrol the security department, ensure that no one is stealing from the armory, and make sure that all prisoners are processed and let out when their time is up.
job-description-zookeeper = Put on a joyful display of cute animals and space carps for all the crew to see. Currently unavailable.
job-description-senior-engineer = Teach new engineers the basics of the station's engine, repairing, atmospherics and power.
job-description-senior-researcher = Teach new scientists the basics of item printing, artifact research and anomalous objects.
job-description-senior-physician = Teach new medics the basics of tending to the wounded, chemistry, diagnosing the diseased and disposing of the dead.
job-description-senior-officer = Teach new officers the basics of searches, performing arrests, prison times and how to properly shoot a firearm.
job-description-zookeeper = Put on a joyful display of cute animals and space carps for all the crew to see. Currently available on Gemini Station.

View File

@ -90,10 +90,6 @@ JobSalvageSpecialist = Salvage Specialist
JobScientist = Scientist
JobSecurityCadet = Security Cadet
JobSecurityOfficer = Security Officer
JobSeniorEngineer = Senior Engineer
JobSeniorOfficer = Senior Officer
JobSeniorPhysician = Senior Physician
JobSeniorResearcher = Senior Researcher
JobServiceWorker = Service Worker
JobStationEngineer = Station Engineer
JobTechnicalAssistant = Technical Assistant

View File

@ -0,0 +1,2 @@
loadout-window = Loadout
loadout-none = None

View File

@ -1,6 +1,6 @@
job-name-gladiator = Gladiator
job-name-guard = Prison Guard
job-name-mail-carrier = Courier
job-name-mail-carrier = Mail Carrier
job-name-martialartist = Martial Artist
job-name-prisoner = Prisoner
job-name-mantis = Psionic Mantis

View File

@ -0,0 +1,192 @@
# Miscellaneous
loadout-group-trinkets = Trinkets
loadout-group-glasses = Glasses
# Command
loadout-group-captain-head = Captain head
loadout-group-captain-jumpsuit = Captain jumpsuit
loadout-group-captain-neck = Captain neck
loadout-group-captain-backpack = Captain backpack
loadout-group-captain-outerclothing = Captain outer clothing
loadout-group-hop-head = Head of Personnel head
loadout-group-hop-jumpsuit = Head of Personnel jumpsuit
loadout-group-hop-neck = Head of Personnel neck
loadout-group-hop-backpack = Head of Personnel backpack
loadout-group-hop-outerclothing = Head of Personnel outer clothing
# Civilian
loadout-group-passenger-jumpsuit = Passenger jumpsuit
loadout-group-passenger-mask = Passenger mask
loadout-group-passenger-gloves = Passenger gloves
loadout-group-passenger-backpack = Passenger backpack
loadout-group-passenger-outerclothing = Passenger outer clothing
loadout-group-passenger-shoes = Passenger shoes
loadout-group-bartender-head = Bartender head
loadout-group-bartender-jumpsuit = Bartender jumpsuit
loadout-group-bartender-outerclothing = Bartender outer clothing
loadout-group-chef-head = Chef head
loadout-group-chef-mask = Chef mask
loadout-group-chef-jumpsuit = Chef jumpsuit
loadout-group-chef-outerclothing = Chef outer clothing
loadout-group-librarian-jumpsuit = Librarian jumpsuit
loadout-group-lawyer-jumpsuit = Lawyer jumpsuit
loadout-group-lawyer-neck = Lawyer neck
loadout-group-lawyer-backpack = Lawyer backpack
loadout-group-chaplain-head = Chaplain head
loadout-group-chaplain-mask = Chaplain mask
loadout-group-chaplain-jumpsuit = Chaplain jumpsuit
loadout-group-chaplain-backpack = Chaplain backpack
loadout-group-chaplain-outerclothing = Chaplain outer clothing
loadout-group-chaplain-neck = Chaplain neck
loadout-group-janitor-head = Janitor head
loadout-group-janitor-jumpsuit = Janitor jumpsuit
loadout-group-janitor-gloves = Janitor gloves
loadout-group-janitor-outerclothing = Janitor outer clothing
loadout-group-botanist-head = Botanist head
loadout-group-botanist-jumpsuit = Botanist jumpsuit
loadout-group-botanist-backpack = Botanist backpack
loadout-group-botanist-outerclothing = Botanist outer clothing
loadout-group-clown-head = Clown head
loadout-group-clown-jumpsuit = Clown jumpsuit
loadout-group-clown-backpack = Clown backpack
loadout-group-clown-outerclothing = Clown outer clothing
loadout-group-clown-shoes = Clown shoes
loadout-group-mime-head = Mime head
loadout-group-mime-mask = Mime mask
loadout-group-mime-jumpsuit = Mime jumpsuit
loadout-group-mime-backpack = Mime backpack
loadout-group-mime-outerclothing = Mime outer clothing
loadout-group-musician-backpack = Musician backpack
loadout-group-musician-outerclothing = Musician outer clothing
# Cargo
loadout-group-quartermaster-head = Logistics Officer head
loadout-group-quartermaster-jumpsuit = Logistics Officer jumpsuit
loadout-group-quartermaster-backpack = Logistics Officer backpack
loadout-group-quartermaster-neck = Logistics Officer neck
loadout-group-quartermaster-outerclothing = Logistics Officer outer clothing
loadout-group-quartermaster-shoes = Logistics Officer shoes
loadout-group-cargo-technician-head = Cargo Technician head
loadout-group-cargo-technician-jumpsuit = Cargo Technician jumpsuit
loadout-group-cargo-technician-backpack = Cargo Technician backpack
loadout-group-cargo-technician-outerclothing = Cargo Technician outer clothing
loadout-group-cargo-technician-shoes = Cargo Technician shoes
loadout-group-salvage-specialist-backpack = Salvage Specialist backpack
loadout-group-salvage-specialist-outerclothing = Salvage Specialist outer clothing
loadout-group-salvage-specialist-shoes = Salvage Specialist shoes
# Engineering
loadout-group-chief-engineer-head = Chief Engineer head
loadout-group-chief-engineer-jumpsuit = Chief Engineer jumpsuit
loadout-group-chief-engineer-backpack = Chief Engineer backpack
loadout-group-chief-engineer-outerclothing = Chief Engineer outer clothing
loadout-group-chief-engineer-neck = Chief Engineer neck
loadout-group-chief-engineer-shoes = Chief Engineer shoes
loadout-group-technical-assistant-jumpsuit = Technical Assistant jumpsuit
loadout-group-station-engineer-head = Station Engineer head
loadout-group-station-engineer-jumpsuit = Station Engineer jumpsuit
loadout-group-station-engineer-backpack = Station Engineer backpack
loadout-group-station-engineer-outerclothing = Station Engineer outer clothing
loadout-group-station-engineer-shoes = Station Engineer shoes
loadout-group-station-engineer-id = Station Engineer ID
loadout-group-atmospheric-technician-jumpsuit = Atmospheric Technician jumpsuit
loadout-group-atmospheric-technician-backpack = Atmospheric Technician backpack
loadout-group-atmospheric-technician-outerclothing = Atmospheric Technician outer clothing
loadout-group-atmospheric-technician-shoes = Atmospheric Technician shoes
# Science
loadout-group-research-director-head = Mystagogue head
loadout-group-research-director-neck = Mystagogue neck
loadout-group-research-director-jumpsuit = Mystagogue jumpsuit
loadout-group-research-director-backpack = Mystagogue backpack
loadout-group-research-director-outerclothing = Mystagogue outer clothing
loadout-group-research-director-shoes = Mystagogue shoes
loadout-group-scientist-head = Scientist head
loadout-group-scientist-neck = Scientist neck
loadout-group-scientist-jumpsuit = Scientist jumpsuit
loadout-group-scientist-backpack = Scientist backpack
loadout-group-scientist-outerclothing = Scientist outer clothing
loadout-group-scientist-gloves = Scientist gloves
loadout-group-scientist-shoes = Scientist shoes
loadout-group-scientist-id = Scientist ID
loadout-group-research-assistant-jumpsuit = Research Assistant jumpsuit
# Security
loadout-group-head-of-security-head = Head of Security head
loadout-group-head-of-security-jumpsuit = Head of Security jumpsuit
loadout-group-head-of-security-neck = Head of Security neck
loadout-group-head-of-security-outerclothing = Head of Security outer clothing
loadout-group-warden-head = Warden head
loadout-group-warden-jumpsuit = Warden jumpsuit
loadout-group-warden-outerclothing = Warden outer clothing
loadout-group-security-head = Security head
loadout-group-security-jumpsuit = Security jumpsuit
loadout-group-security-backpack = Security backpack
loadout-group-security-belt = Security Belt
loadout-group-security-outerclothing = Security outer clothing
loadout-group-security-shoes = Security shoes
loadout-group-security-id = Security ID
loadout-group-detective-head = Detective head
loadout-group-detective-neck = Detective neck
loadout-group-detective-jumpsuit = Detective jumpsuit
loadout-group-detective-backpack = Detective backpack
loadout-group-detective-outerclothing = Detective outer clothing
loadout-group-security-cadet-jumpsuit = Security cadet jumpsuit
# Medical
loadout-group-medical-gloves = Medical gloves
loadout-group-medical-mask = Medical mask
loadout-group-chief-medical-officer-head = Chief Medical Officer head
loadout-group-chief-medical-officer-jumpsuit = Chief Medical Officer jumpsuit
loadout-group-chief-medical-officer-outerclothing = Chief Medical Officer outer clothing
loadout-group-chief-medical-officer-backpack = Chief Medical Officer backpack
loadout-group-chief-medical-officer-shoes = Chief Medical Officer shoes
loadout-group-chief-medical-officer-neck = Chief Medical Officer neck
loadout-group-medical-doctor-head = Medical Doctor head
loadout-group-medical-doctor-jumpsuit = Medical Doctor jumpsuit
loadout-group-medical-doctor-outerclothing = Medical Doctor outer clothing
loadout-group-medical-doctor-backpack = Medical Doctor backpack
loadout-group-medical-doctor-shoes = Medical Doctor shoes
loadout-group-medical-doctor-id = Medical Doctor ID
loadout-group-medical-intern-jumpsuit = Medical intern jumpsuit
loadout-group-chemist-jumpsuit = Chemist jumpsuit
loadout-group-chemist-outerclothing = Chemist outer clothing
loadout-group-chemist-backpack = Chemist backpack
loadout-group-paramedic-head = Paramedic head
loadout-group-paramedic-jumpsuit = Paramedic jumpsuit
loadout-group-paramedic-outerclothing = Paramedic outer clothing
loadout-group-paramedic-shoes = Paramedic shoes
loadout-group-paramedic-backpack = Paramedic backpack
# Wildcards
loadout-group-reporter-jumpsuit = Reporter jumpsuit
loadout-group-boxer-jumpsuit = Boxer jumpsuit
loadout-group-boxer-gloves = Boxer gloves

View File

@ -0,0 +1,7 @@
# Restrictions
loadout-restrictions = Restrictions
loadouts-min-limit = Min count: {$count}
loadouts-max-limit = Max count: {$count}
loadouts-points-limit = Points: {$count} / {$max}
loadouts-points-restriction = Insufficient points

View File

@ -19,8 +19,6 @@ humanoid-profile-editor-pronouns-neuter-text = It / It
humanoid-profile-editor-import-button = Import
humanoid-profile-editor-export-button = Export
humanoid-profile-editor-save-button = Save
humanoid-profile-editor-clothing-label = Clothing:
humanoid-profile-editor-backpack-label = Backpack:
humanoid-profile-editor-spawn-priority-label = Spawn priority:
humanoid-profile-editor-eyes-label = Eye color:
humanoid-profile-editor-jobs-tab = Jobs

View File

@ -106,3 +106,6 @@ MagicalLamp: Lamp
# 2024-04-21
ClothingEyesHudSyndicateMed: ClothingEyesHudSyndicateAgent
# 2024-04-23
SpawnPointMailCarrier: SpawnPointCourier

View File

@ -88,6 +88,12 @@ GeneratorUranium: PortableGeneratorSuperPacman
GeneratorPlasmaMachineCircuitboard: PortableGeneratorPacmanMachineCircuitboard
GeneratorUraniumMachineCircuitboard: PortableGeneratorSuperPacmanMachineCircuitboard
# 2023-12-10
SpawnPointSeniorResearcher: null
SpawnPointSeniorOfficer: null
SpawnPointSeniorEngineer: null
SpawnPointSeniorPhysician: null
# 2023-12-12
#No this is not the CMO hardsuit, their prototype IDs were just confusingly similar
ClothingOuterHardsuitMedic: ClothingOuterHardsuitSyndieMedic

View File

@ -104,6 +104,17 @@
- id: Flash
#- id: TelescopicBaton
- type: entity
noSpawn: true
parent: ClothingBackpackIan
id: ClothingBackpackHOPIanFilled
components:
- type: StorageFill
contents:
- id: BoxSurvival
- id: Flash
#- id: TelescopicBaton
- type: entity
noSpawn: true
parent: ClothingBackpackMedical
@ -202,7 +213,7 @@
- id: BoxSurvival
- id: Bible
- id: RubberStampChaplain
- type: entity
noSpawn: true
parent: ClothingBackpack

View File

@ -54,6 +54,19 @@
- id: Handcuffs
- id: Handcuffs
- type: entity
id: ClothingBeltSecurityWebbingFilled
parent: ClothingBeltSecurityWebbing
suffix: Filled
components:
- type: StorageFill
contents:
- id: GrenadeFlashBang
- id: TearGasGrenade
- id: Stunbaton
- id: Handcuffs
- id: Handcuffs
- type: entity
id: ClothingBeltJanitorFilled
parent: ClothingBeltJanitor

View File

@ -1695,7 +1695,6 @@
whitelist:
- Zookeeper
- Scientist
- SeniorResearcher
- ResearchDirector
- Chef

View File

@ -1,4 +1,3 @@
- type: entity
id: SpawnPointMedicalBorg
parent: SpawnPointJobBase
@ -13,3 +12,16 @@
state: medical
- sprite: Mobs/Silicon/chassis.rsi
state: medical_e
- type: entity
id: SpawnPointCourier
parent: SpawnPointJobBase
name: courier
components:
- type: SpawnPoint
job_id: Courier
- type: Sprite
layers:
- state: green
- sprite: DeltaV/Markers/jobs.rsi
state: courier

View File

@ -124,7 +124,17 @@
baseSprintSpeed: 5.0
- type: Inventory
speciesId: harpy
templateId: digitigrade
# templateId: digitigrade
displacements:
jumpsuit:
layer:
sprite: DeltaV/Mobs/Species/Harpy/displacement.rsi
state: jumpsuit
copyToShaderParameters:
# Value required, provide a dummy. Gets overridden when applied.
layerKey: dummy
parameterTexture: displacementMap
parameterUV: displacementUV
- type: HarpyVisuals
- type: UltraVision
@ -140,6 +150,16 @@
species: Harpy
- type: Inventory
speciesId: harpy
displacements:
jumpsuit:
layer:
sprite: DeltaV/Mobs/Species/Harpy/displacement.rsi
state: jumpsuit
copyToShaderParameters:
# Value required, provide a dummy. Gets overridden when applied.
layerKey: dummy
parameterTexture: displacementMap
parameterUV: displacementUV
- type: Sprite
scale: 0.9, 0.9
layers:

View File

@ -52,3 +52,31 @@
whitelist:
tags:
- Write
- type: entity
parent: BasePDA
id: CourierPDA
name: courier PDA
description: Smells like unopened letters.
components:
- type: Sprite
sprite: DeltaV/Objects/Devices/pda.rsi
layers:
- map: [ "enum.PdaVisualLayers.Base" ]
- state: "light_overlay"
map: [ "enum.PdaVisualLayers.Flashlight" ]
shader: "unshaded"
visible: false
- state: "id_overlay"
map: [ "enum.PdaVisualLayers.IdLight" ]
shader: "unshaded"
visible: false
- type: Pda
id: CourierIDCard
state: pda-mailcarrier
- type: PdaBorderColor
borderColor: "#e39751"
accentVColor: "#050c4d"
- type: Icon
sprite: DeltaV/Objects/Devices/pda.rsi
state: pda-mailcarrier

View File

@ -0,0 +1,12 @@
- type: entity
parent: IDCardStandard
id: CourierIDCard
name: courier ID card
components:
- type: Sprite
layers:
- state: default
- sprite: DeltaV/Objects/Misc/id_cards.rsi
state: nyanomailcarrier
- type: PresetIdCard
job: Courier

View File

@ -0,0 +1,57 @@
# Head
- type: loadout
id: CourierHead
equipment: CourierHead
- type: startingGear
id: CourierHead
equipment:
head: ClothingHeadCourier
# Jumpsuit
- type: loadout
id: CourierJumpsuit
equipment: CourierJumpsuit
- type: startingGear
id: CourierJumpsuit
equipment:
jumpsuit: ClothingUniformCourier
- type: loadout
id: CourierJumpskirt
equipment: CourierJumpskirt
- type: startingGear
id: CourierJumpskirt
equipment:
jumpsuit: ClothingUniformSkirtCourier
# Shoes
- type: loadout
id: RollerSkates
equipment: RollerSkates
- type: startingGear
id: RollerSkates
equipment:
shoes: ClothingShoesSkates
# PDA
- type: loadout
id: CourierPDA
equipment: CourierPDA
- type: startingGear
id: CourierPDA
equipment:
id: CourierPDA
- type: loadout
id: MailCarrierPDA
equipment: MailCarrierPDA
- type: startingGear
id: MailCarrierPDA
equipment:
id: MailCarrierPDA

View File

@ -0,0 +1,56 @@
# Head
- type: loadout
id: CorpsmanBeret
equipment: CorpsmanBeret
- type: startingGear
id: CorpsmanBeret
equipment:
head: ClothingHeadHatBeretCorpsman
# Jumpsuit
- type: loadout
id: BrigMedicJumpsuit
equipment: BrigMedicJumpsuit
- type: startingGear
id: BrigMedicJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitBrigmedic
- type: loadout
id: BrigMedicJumpskirt
equipment: BrigMedicJumpskirt
- type: startingGear
id: BrigMedicJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtBrigmedic
# Back
- type: loadout
id: BrigMedicBackpack
equipment: BrigMedicBackpack
- type: startingGear
id: BrigMedicBackpack
equipment:
back: ClothingBackpackBrigmedicDeltaVFilled
- type: loadout
id: BrigMedicSatchel
equipment: BrigMedicSatchel
- type: startingGear
id: BrigMedicSatchel
equipment:
back: ClothingBackpackSatchelBrigmedicDeltaVFilled
- type: loadout
id: BrigMedicDuffel
equipment: BrigMedicDuffel
- type: startingGear
id: BrigMedicDuffel
equipment:
back: ClothingBackpackDuffelBrigmedicDeltaVFilled

View File

@ -0,0 +1,65 @@
# Logistics
## Courier
- type: loadoutGroup
id: CourierHead
name: loadout-group-courier-head
minLimit: 0
loadouts:
- CourierHead
- MailCarrierHead
- type: loadoutGroup
id: CourierJumpsuit
name: loadout-group-courier-jumpsuit
loadouts:
- CourierJumpsuit
- CourierJumpskirt
- MailCarrierJumpsuit
- MailCarrierJumpskirt
- type: loadoutGroup
id: CourierOuterClothing
name: loadout-group-courier-outerclothing
minLimit: 0
loadouts:
- CargoTechnicianWintercoat
- MailCarrierWintercoat
- type: loadoutGroup
id: CourierShoes
name: loadout-group-courier-shoes
loadouts:
- BlackShoes
- CargoWinterBoots
- RollerSkates
- type: loadoutGroup
id: CourierPDA
name: loadout-group-courier-id
loadouts:
- CourierPDA
- MailCarrierPDA
# Security
## Brig Medic
- type: loadoutGroup
id: BrigMedicHead
name: loadout-group-brig-medic-head
minLimit: 0
loadouts:
- CorpsmanBeret
- type: loadoutGroup
id: BrigMedicJumpsuit
name: loadout-group-brig-medic-jumpsuit
loadouts:
- BrigMedicJumpsuit
- BrigMedicJumpskirt
- type: loadoutGroup
id: BrigMedicBack
name: loadout-group-brig-medic-back
loadouts:
- BrigMedicBackpack
- BrigMedicSatchel
- BrigMedicDuffel

View File

@ -0,0 +1,23 @@
# Logistics
- type: roleLoadout
id: JobCourier
groups:
- CourierHead
- CourierJumpsuit
- CourierOuterClothing
- CourierPDA
- CargoTechnicianBackpack
- CourierShoes
- Trinkets
# Security
- type: roleLoadout
id: JobBrigmedic
groups:
- BrigMedicHead
- BrigMedicJumpsuit
- BrigMedicBack
- SecurityShoes
- SecurityOuterClothing
- MedicalGloves
- Trinkets

View File

@ -1,13 +1,19 @@
- type: job
id: Courier
name: job-name-courier
description: job-description-courier
startingGear: CourierGear
playTimeTracker: JobCourier
icon: "JobIconCourier"
supervisors: job-supervisors-qm
access:
- Cargo
- Maintenance
- Mail
- type: startingGear
id: CourierGear
equipment:
head: ClothingHeadCourier
jumpsuit: ClothingUniformCourier
back: ClothingBackpackFilled
shoes: ClothingShoesColorBlack
id: MailCarrierPDA
ears: ClothingHeadsetCargo
belt: CourierBag
innerClothingSkirt: ClothingUniformSkirtCourier
satchel: ClothingBackpackSatchelFilled
duffelbag: ClothingBackpackDuffelFilled

View File

@ -66,7 +66,7 @@
gloves: ClothingHandsGlovesCombat
shoes: ClothingShoesSlippers
id: SyndiListeningPostPDA
innerClothingSkirt: ClothingUniformJumpsuitPyjamaSyndicatePink
# innerClothingSkirt: ClothingUniformJumpsuitPyjamaSyndicatePink # I don't think loadouts work for ghost roles anyways
# Mobsters
@ -81,9 +81,9 @@
outerClothing: ClothingOuterArmorBasicSlim
id: PassengerPDA
belt: ClothingBeltHolster
innerClothingSkirt: ClothingUniformJumpsuitSuitBlackMob
satchel: ClothingBackpackMafiaFilled
duffelbag: ClothingBackpackMafiaFilled
# innerClothingSkirt: ClothingUniformJumpsuitSuitBlackMob # TODO: Loadouts
# satchel: ClothingBackpackMafiaFilled
# duffelbag: ClothingBackpackMafiaFilled
- type: startingGear
id: MobsterGearAlt
@ -97,6 +97,6 @@
id: PassengerPDA
belt: ClothingBeltSuspendersBlack
pocket1: CombatKnife
innerClothingSkirt: ClothingUniformJumpsuitSuitBrown
satchel: ClothingBackpackMafiaFilled
duffelbag: ClothingBackpackMafiaFilled
# innerClothingSkirt: ClothingUniformJumpsuitSuitBrown # TODO: Loadouts
# satchel: ClothingBackpackMafiaFilled
# duffelbag: ClothingBackpackMafiaFilled

View File

@ -30,17 +30,8 @@
- type: startingGear
id: CorpsmanGear # see Prototypes/Roles/Jobs/Fun/misc_startinggear.yml for "BrigmedicGear"
equipment:
jumpsuit: ClothingUniformJumpsuitBrigmedic
outerClothing: ClothingOuterArmorPlateCarrier
back: ClothingBackpackBrigmedicDeltaVFilled
shoes: ClothingShoesBootsCombatFilled
gloves: ClothingHandsGlovesNitrile
eyes: ClothingEyesHudMedical
head: ClothingHeadHatBeretCorpsman
id: CorpsmanPDA
ears: ClothingHeadsetBrigmedic
belt: ClothingBeltCorpsmanWebbingFilled
pocket1: WeaponPistolMk58Nonlethal
innerClothingSkirt: ClothingUniformJumpskirtBrigmedic
satchel: ClothingBackpackSatchelBrigmedicDeltaVFilled
duffelbag: ClothingBackpackDuffelBrigmedicDeltaVFilled

View File

@ -3,3 +3,6 @@
- type: playTimeTracker
id: JobMedicalBorg
- type: playTimeTracker
id: JobCourier

View File

@ -0,0 +1,6 @@
- type: statusIcon
parent: JobIcon
id: JobIconCourier
icon:
sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi
state: nyanoMailCarrier

Some files were not shown because too many files have changed in this diff Show More