Merge remote-tracking branch 'space-station-14/master' into 17-04-2024-upstream

# Conflicts:
#	Content.Client/Movement/Systems/WaddleAnimationSystem.cs
#	Content.Server/IoC/ServerContentIoC.cs
#	Resources/Changelog/Changelog.yml
#	Resources/Prototypes/Maps/europa.yml
#	Resources/Prototypes/Maps/train.yml
#	Resources/Textures/_CP14/Clothing/Masks/pluto-mask.rsi/meta.json
#	Resources/Textures/_CP14/Objects/Weapons/Melee/Dagger/dagger.rsi/meta.json
#	Resources/Textures/_CP14/Objects/Weapons/Melee/HandheldAxe/handheldAxe.rsi/meta.json
#	Resources/Textures/_CP14/Objects/Weapons/Melee/Sickle/sickle.rsi/meta.json
#	Resources/Textures/_CP14/Objects/Weapons/Melee/TwoHandedSword/zweichhender.rsi/meta.json
This commit is contained in:
Ed
2024-04-17 11:16:24 +03:00
179 changed files with 15229 additions and 1450 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

@@ -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

@@ -1,7 +1,13 @@
using System.Numerics;
using Content.Client.Buckle;
using Content.Client.Gravity;
using Content.Shared.ActionBlocker;
using Content.Shared.Buckle.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
@@ -14,6 +20,9 @@ public sealed class WaddleAnimationSystem : EntitySystem
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly GravitySystem _gravity = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly BuckleSystem _buckle = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
public override void Initialize()
{
@@ -21,6 +30,9 @@ public sealed class WaddleAnimationSystem : EntitySystem
SubscribeLocalEvent<WaddleAnimationComponent, StartedWaddlingEvent>(OnStartedWalking);
SubscribeLocalEvent<WaddleAnimationComponent, StoppedWaddlingEvent>(OnStoppedWalking);
SubscribeLocalEvent<WaddleAnimationComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<WaddleAnimationComponent, StunnedEvent>(OnStunned);
SubscribeLocalEvent<WaddleAnimationComponent, KnockedDownEvent>(OnKnockedDown);
SubscribeLocalEvent<WaddleAnimationComponent, BuckleChangeEvent>(OnBuckleChange);
}
private void OnMovementInput(EntityUid entity, WaddleAnimationComponent component, MoveInputEvent args)
@@ -34,8 +46,6 @@ public sealed class WaddleAnimationSystem : EntitySystem
if (!args.HasDirectionalMovement && component.IsCurrentlyWaddling)
{
component.IsCurrentlyWaddling = false;
var stopped = new StoppedWaddlingEvent(entity);
RaiseLocalEvent(entity, ref stopped);
@@ -47,8 +57,6 @@ public sealed class WaddleAnimationSystem : EntitySystem
if (component.IsCurrentlyWaddling || !args.HasDirectionalMovement)
return;
component.IsCurrentlyWaddling = true;
var started = new StartedWaddlingEvent(entity);
RaiseLocalEvent(entity, ref started);
@@ -57,19 +65,25 @@ public sealed class WaddleAnimationSystem : EntitySystem
private void OnStartedWalking(EntityUid uid, WaddleAnimationComponent component, StartedWaddlingEvent args)
{
if (_animation.HasRunningAnimation(uid, component.KeyName))
{
return;
}
if (!TryComp<InputMoverComponent>(uid, out var mover))
{
return;
}
if (_gravity.IsWeightless(uid))
{
return;
}
if (!_actionBlocker.CanMove(uid, mover))
return;
// Do nothing if buckled in
if (_buckle.IsBuckled(uid))
return;
// Do nothing if crit or dead (for obvious reasons)
if (_mobState.IsIncapacitated(uid))
return;
var tumbleIntensity = component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity;
var len = mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength;
@@ -114,6 +128,36 @@ public sealed class WaddleAnimationSystem : EntitySystem
private void OnStoppedWalking(EntityUid uid, WaddleAnimationComponent component, StoppedWaddlingEvent args)
{
StopWaddling(uid, component);
}
private void OnAnimationCompleted(EntityUid uid, WaddleAnimationComponent component, AnimationCompletedEvent args)
{
var started = new StartedWaddlingEvent(uid);
RaiseLocalEvent(uid, ref started);
}
private void OnStunned(EntityUid uid, WaddleAnimationComponent component, StunnedEvent args)
{
StopWaddling(uid, component);
}
private void OnKnockedDown(EntityUid uid, WaddleAnimationComponent component, KnockedDownEvent args)
{
StopWaddling(uid, component);
}
private void OnBuckleChange(EntityUid uid, WaddleAnimationComponent component, BuckleChangeEvent args)
{
StopWaddling(uid, component);
}
private void StopWaddling(EntityUid uid, WaddleAnimationComponent component)
{
if (!component.IsCurrentlyWaddling)
return;
_animation.Stop(uid, component.KeyName);
if (!TryComp<SpriteComponent>(uid, out var sprite))
@@ -123,13 +167,7 @@ public sealed class WaddleAnimationSystem : EntitySystem
sprite.Offset = new Vector2();
sprite.Rotation = Angle.FromDegrees(0);
component.IsCurrentlyWaddling = false;
}
private void OnAnimationCompleted(EntityUid uid, WaddleAnimationComponent component, AnimationCompletedEvent args)
{
var started = new StartedWaddlingEvent(uid);
RaiseLocalEvent(uid, ref started);
}
}

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 class JobRequirementsManager
public sealed class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
@@ -133,5 +134,13 @@ public sealed 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,
@@ -148,8 +143,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 +179,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))
{
@@ -627,10 +595,15 @@ namespace Content.Client.Preferences.UI
category.AddChild(selector);
_jobPriorities.Add(selector);
selector.LoadoutUpdated += args =>
{
Profile = Profile?.WithLoadout(args);
SetDirty();
};
selector.PriorityChanged += priority =>
{
Profile = Profile?.WithJobPriority(job.ID, priority);
IsDirty = true;
foreach (var jobSelector in _jobPriorities)
{
@@ -646,6 +619,8 @@ namespace Content.Client.Preferences.UI
Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
}
}
SetDirty();
};
}
@@ -663,7 +638,7 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithFlavorText(content);
IsDirty = true;
SetDirty();
}
private void OnMarkingChange(MarkingSet markings)
@@ -672,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;
@@ -737,6 +704,9 @@ namespace Content.Client.Preferences.UI
}
IsDirty = true;
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadProfile();
}
protected override void Dispose(bool disposing)
@@ -745,39 +715,28 @@ 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();
UpdateRoleRequirements();
UpdateControls();
_needUpdatePreview = true;
ShowClothes.Pressed = true;
}
private void SetAge(int newAge)
{
Profile = Profile?.WithAge(newAge);
IsDirty = true;
SetDirty();
}
private void SetSex(Sex newSex)
@@ -798,13 +757,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)
@@ -813,46 +772,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
@@ -861,7 +808,6 @@ namespace Content.Client.Preferences.UI
set
{
_isDirty = value;
_needUpdatePreview = true;
UpdateSaveButton();
}
}
@@ -981,7 +927,7 @@ namespace Content.Client.Preferences.UI
if (!_prototypeManager.HasIndex<GuideEntryPrototype>(species))
return;
var style = speciesProto.GuideBookIcon;
const string style = "SpeciesInfoDefault";
SpeciesInfoButton.StyleClasses.Add(style);
}
@@ -1017,26 +963,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)
@@ -1166,13 +1092,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()
@@ -1184,17 +1110,15 @@ namespace Content.Client.Preferences.UI
UpdateGenderControls();
UpdateSkinColor();
UpdateSpecies();
UpdateClothingControls();
UpdateBackpackControls();
UpdateSpawnPriorityControls();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateLoadouts();
UpdateJobPriorities();
UpdateAntagPreferences();
UpdateTraitPreferences();
UpdateMarkings();
RebuildSpriteView();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
@@ -1202,17 +1126,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)
@@ -1225,143 +1138,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();
}
}
@@ -1386,41 +1167,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.
@@ -875,8 +966,35 @@ namespace Content.Server.Database
public byte[] Data { get; set; } = default!;
}
// Note: this interface isn't used by the game, but it *is* used by SS14.Admin.
// Don't remove! Or face the consequences!
public interface IAdminRemarksCommon
{
public int Id { get; }
public int? RoundId { get; }
public Round? Round { get; }
public Guid? PlayerUserId { get; }
public Player? Player { get; }
public TimeSpan PlaytimeAtNote { get; }
public string Message { get; }
public Player? CreatedBy { get; }
public DateTime CreatedAt { get; }
public Player? LastEditedBy { get; }
public DateTime? LastEditedAt { get; }
public DateTime? ExpirationTime { get; }
public bool Deleted { get; }
}
[Index(nameof(PlayerUserId))]
public class AdminNote
public class AdminNote : IAdminRemarksCommon
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
@@ -910,7 +1028,7 @@ namespace Content.Server.Database
}
[Index(nameof(PlayerUserId))]
public class AdminWatchlist
public class AdminWatchlist : IAdminRemarksCommon
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
@@ -941,7 +1059,7 @@ namespace Content.Server.Database
}
[Index(nameof(PlayerUserId))]
public class AdminMessage
public class AdminMessage : IAdminRemarksCommon
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }

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

@@ -149,12 +149,12 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
MetaData.SetEntityName(uid, $"solution - {name}");
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);
}
#region Event Handlers
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)

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

@@ -0,0 +1,32 @@
using Content.Shared.Body.Components;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
namespace Content.Server.Destructible.Thresholds.Behaviors;
[UsedImplicitly]
[DataDefinition]
public sealed partial class BurnBodyBehavior : IThresholdBehavior
{
public void Execute(EntityUid bodyId, DestructibleSystem system, EntityUid? cause = null)
{
var transformSystem = system.EntityManager.System<TransformSystem>();
var inventorySystem = system.EntityManager.System<InventorySystem>();
var sharedPopupSystem = system.EntityManager.System<SharedPopupSystem>();
if (!system.EntityManager.TryGetComponent<InventoryComponent>(bodyId, out var comp))
return;
foreach (var item in inventorySystem.GetHandOrInventoryEntities(bodyId))
{
transformSystem.DropNextTo(item, bodyId);
}
sharedPopupSystem.PopupCoordinates(Loc.GetString("bodyburn-text-others", ("name", bodyId)), transformSystem.GetMoverCoordinates(bodyId), PopupType.LargeCaution);
system.EntityManager.QueueDeleteEntity(bodyId);
}
}

View File

@@ -23,7 +23,10 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
public float Offset { get; set; } = 0.5f;
[DataField("transferForensics")]
public bool DoTransferForensics = false;
public bool DoTransferForensics;
[DataField]
public bool SpawnInContainer;
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
@@ -49,7 +52,9 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.ComponentFactory))
{
var spawned = system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
var spawned = SpawnInContainer
? system.EntityManager.SpawnNextToOrDrop(entityId, owner)
: system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
system.StackSystem.SetCount(spawned, count);
TransferForensics(spawned, system, owner);
@@ -58,7 +63,9 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
{
for (var i = 0; i < count; i++)
{
var spawned = system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
var spawned = SpawnInContainer
? system.EntityManager.SpawnNextToOrDrop(entityId, owner)
: system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
TransferForensics(spawned, system, owner);
}

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

@@ -249,7 +249,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

@@ -62,6 +62,7 @@ public class IdentitySystem : SharedIdentitySystem
{
var ident = Spawn(null, Transform(uid).Coordinates);
_metaData.SetEntityName(ident, "identity");
QueueIdentityUpdate(uid);
_container.Insert(ident, component.IdentityEntitySlot);
}

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

@@ -357,16 +357,17 @@ public sealed class MindSystem : SharedMindSystem
mind.UserId = userId;
mind.OriginalOwnerUserId ??= userId;
// The UserId may not have a current session, but user data may still exist for disconnected players.
// So we cannot combine this with the TryGetSessionById() check below.
if (_players.GetPlayerData(userId.Value).ContentData() is { } data)
data.Mind = mindId;
if (_players.TryGetSessionById(userId.Value, out var ret))
{
mind.Session = ret;
_pvsOverride.AddSessionOverride(netMind, ret);
_players.SetAttachedEntity(ret, mind.CurrentEntity);
}
// session may be null, but user data may still exist for disconnected players.
if (_players.GetPlayerData(userId.Value).ContentData() is { } data)
data.Mind = mindId;
}
public void ControlMob(EntityUid user, EntityUid target)

View File

@@ -54,7 +54,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 class PlayTimeTrackingManager
public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IServerNetManager _net = default!;
@@ -201,6 +201,11 @@ public sealed 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

@@ -51,7 +51,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;
@@ -10,10 +11,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;
@@ -86,7 +89,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);
@@ -101,9 +104,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);
}
}
}
@@ -134,7 +139,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)
@@ -176,11 +181,49 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
if (prototype?.StartingGear != null)
{
var startingGear = _prototypeManager.Index<StartingGearPrototype>(prototype.StartingGear);
EquipStartingGear(entity.Value, startingGear, profile);
EquipStartingGear(entity.Value, startingGear);
if (profile != null)
EquipIdCard(entity.Value, profile.Name, prototype, station);
}
// 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)
{
_humanoidSystem.LoadProfile(entity.Value, profile);

View File

@@ -57,6 +57,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
[Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
[Dependency] protected readonly SharedHandsSystem Hands = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] protected readonly MetaDataSystem MetaData = default!;
public override void Initialize()
{

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

@@ -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

@@ -26,6 +26,7 @@ public abstract class SharedMindSystem : EntitySystem
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedPlayerSystem _player = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly ISharedPlayerManager _playerMan = default!;
[ViewVariables]
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
@@ -107,6 +108,7 @@ public abstract class SharedMindSystem : EntitySystem
TryComp(mindIdValue, out mind))
{
DebugTools.Assert(mind.UserId == user);
mindId = mindIdValue;
return true;
}
@@ -422,29 +424,26 @@ public abstract class SharedMindSystem : EntitySystem
return TryComp(mindId, out mind);
}
public bool TryGetMind(
ContentPlayerData contentPlayer,
out EntityUid mindId,
[NotNullWhen(true)] out MindComponent? mind)
{
mindId = contentPlayer.Mind ?? default;
return TryComp(mindId, out mind);
}
// TODO MIND make this return a nullable EntityUid or Entity<MindComponent>
public bool TryGetMind(
ICommonSession? player,
out EntityUid mindId,
[NotNullWhen(true)] out MindComponent? mind)
{
mindId = default;
mind = null;
if (_player.ContentData(player) is not { } data)
if (player == null)
{
mindId = default;
mind = null;
return false;
}
if (TryGetMind(data, out mindId, out mind))
if (TryGetMind(player.UserId, out var mindUid, out mind))
{
mindId = mindUid.Value;
return true;
}
DebugTools.AssertNull(data.Mind);
mindId = default;
return false;
}

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;
@@ -31,6 +33,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,
@@ -39,13 +46,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;
@@ -54,13 +60,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>
@@ -68,15 +73,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))
{
}
@@ -88,15 +94,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))
{
}
@@ -113,8 +118,6 @@ namespace Content.Shared.Preferences
Sex.Male,
Gender.Male,
new HumanoidCharacterAppearance(),
ClothingPreference.Jumpsuit,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@@ -122,7 +125,8 @@ namespace Content.Shared.Preferences
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>())
new List<string>(),
new Dictionary<string, RoleLoadout>())
{
}
@@ -141,8 +145,6 @@ namespace Content.Shared.Preferences
Sex.Male,
Gender.Male,
HumanoidCharacterAppearance.DefaultWithSpecies(species),
ClothingPreference.Jumpsuit,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@@ -150,7 +152,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.
@@ -195,11 +198,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; }
@@ -219,8 +222,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;
@@ -263,21 +264,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)
@@ -291,7 +285,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)
@@ -301,7 +295,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)
@@ -321,7 +315,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)
@@ -343,7 +338,7 @@ namespace Content.Shared.Preferences
list.Remove(traitId);
}
}
return new(this, _jobPriorities, _antagPreferences, list);
return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
}
public string Summary =>
@@ -362,17 +357,19 @@ namespace Content.Shared.Preferences
if (Sex != other.Sex) return false;
if (Gender != other.Gender) 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;
@@ -455,21 +452,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,
@@ -502,8 +484,6 @@ namespace Content.Shared.Preferences
Sex = sex;
Gender = gender;
Appearance = appearance;
Clothing = clothing;
Backpack = backpack;
SpawnPriority = spawnPriority;
_jobPriorities.Clear();
@@ -520,12 +500,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;
}
@@ -551,16 +550,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,26 @@
using System.Diagnostics.CodeAnalysis;
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);
return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason,
collection.Resolve<IEntityManager>(),
collection.Resolve<IPrototypeManager>());
}
}

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 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 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 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 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

@@ -96,7 +96,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)
@@ -162,7 +162,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

@@ -9,37 +9,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))
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

@@ -59,7 +59,7 @@ public sealed class StepTriggerSystem : EntitySystem
if (component.Blacklist != null && TryComp<MapGridComponent>(transform.GridUid, out var grid))
{
var positon = _map.LocalToTile(uid, grid, transform.Coordinates);
var positon = _map.LocalToTile(transform.GridUid.Value, grid, transform.Coordinates);
var anch = _map.GetAnchoredEntitiesEnumerator(uid, grid, positon);
while (anch.MoveNext(out var ent))

View File

@@ -1,88 +1,4 @@
Entries:
- author: Varen
changes:
- message: Cutting wires will now properly electrocute if enough power is available.
type: Fix
id: 5855
time: '2024-02-01T11:54:25.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24554
- author: SlamBamActionman
changes:
- message: Paper is now edible, even for non-moths.
type: Tweak
id: 5856
time: '2024-02-01T12:40:55.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24755
- author: mac6na6na
changes:
- message: Tiered part crates will no longer appear on salvage expeditions.
type: Remove
id: 5857
time: '2024-02-01T12:41:03.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24810
- author: mirrorcult
changes:
- message: Crayon/lathe/vendor/round end/gas analyzer windows now open closer to
the sides of the screen
type: Tweak
id: 5858
time: '2024-02-01T12:49:49.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24767
- author: Tayrtahn
changes:
- message: Plushies, whoopie cushions, and a few other noisemaking objects can be
used as modular grenade payloads.
type: Add
id: 5859
time: '2024-02-01T12:59:42.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24306
- author: Repo
changes:
- message: AFK admins will trigger the SOS in aHelp relay.
type: Tweak
id: 5860
time: '2024-02-01T13:03:50.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24482
- author: Nopey
changes:
- message: Many belts and the plant bag no longer produce an erroneous "Can't insert"
and "This doesn't go in there!" messages while inserting certain items such
as Produce (plant bag) or Smoke Grenades (security belt).
type: Fix
- message: Ammunition can once again be stored in assault belts, as was the case
before June 2022.
type: Fix
id: 5861
time: '2024-02-01T13:33:57.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24063
- author: Blazeror
changes:
- message: Chemical analysis goggles can now inspect solutions inside spray bottles.
type: Fix
id: 5862
time: '2024-02-01T22:19:01.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24838
- author: themias
changes:
- message: Fixed dylovene overdoses not applying brute damage
type: Fix
id: 5863
time: '2024-02-01T22:21:04.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24826
- author: themias
changes:
- message: Added sound effect for Diona salutes
type: Add
id: 5864
time: '2024-02-01T22:34:09.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24836
- author: Dutch-VanDerLinde
changes:
- message: Detectives are now listed under civilian instead of security.
type: Tweak
id: 5865
time: '2024-02-01T22:49:54.0000000+00:00'
url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24739
- author: VasilisThePikachu
changes:
- message: The fridge's explosion resistance has been nerfed significantly, It's
@@ -3839,3 +3755,82 @@
id: 6354
time: '2024-04-14T12:12:54.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26338
- author: beck-thompson
changes:
- message: Cybersun pen now makes a slashing noise when doing damage.
type: Fix
id: 6355
time: '2024-04-14T20:04:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26951
- author: TokenStyle
changes:
- message: Lockers cannot be deconstructed with a screwdriver when locked now.
type: Fix
id: 6356
time: '2024-04-14T22:26:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26961
- author: pissdemon
changes:
- message: Catwalks over lava are slightly less likely to catch you on fire again.
type: Fix
id: 6357
time: '2024-04-15T01:27:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26968
- author: Velcroboy
changes:
- message: Fixed sec/lawyer and vault airlocks
type: Fix
id: 6358
time: '2024-04-15T22:22:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26980
- author: tosatur
changes:
- message: Made clown snoring quieter
type: Tweak
id: 6359
time: '2024-04-16T18:48:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27012
- author: DEATHB4DEFEAT, Dutch-VanDerLinde, metalgearsloth and musicmanvr
changes:
- message: Added loadouts
type: Add
id: 6360
time: '2024-04-16T19:57:41.736477+00:00'
url: null
- author: Dutch-VanDerLinde
changes:
- message: Senior role ID cards now function properly.
type: Fix
id: 6361
time: '2024-04-16T20:17:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27017
- author: Dutch-VanDerLinde
changes:
- message: Most job loadouts now have winter clothing available.
type: Tweak
id: 6362
time: '2024-04-17T02:49:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27022
- author: metalgearsloth
changes:
- message: 'Fix the following in lobby: ShowClothes button not working, Skin Color
not updating, Skin Color slider not updating upon closing and re-opening character.'
type: Fix
id: 6363
time: '2024-04-17T02:54:54.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27033
- author: MACMAN2003
changes:
- message: Nuclear operatives now only need 20 players to be readied up again instead
of 35.
type: Tweak
id: 6364
time: '2024-04-17T03:19:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27036
- author: Bellwether
changes:
- message: The nun hood now appears in the Chaplain's loadout.
type: Tweak
id: 6365
time: '2024-04-17T03:36:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27025

View File

@@ -0,0 +1 @@
bodyburn-text-others = {$name}'s body burns to ash!

View File

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

View File

@@ -0,0 +1,184 @@
# Miscellaneous
loadout-group-trinkets = Trinkets
# 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-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-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 = Quartermaster head
loadout-group-quartermaster-jumpsuit = Quartermaster jumpsuit
loadout-group-quartermaster-backpack = Quartermaster backpack
loadout-group-quartermaster-neck = Quartermaster neck
loadout-group-quartermaster-outerclothing = Quartermaster outer clothing
loadout-group-quartermaster-shoes = Quartermaster 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 = Research Director head
loadout-group-research-director-neck = Research Director neck
loadout-group-research-director-jumpsuit = Research Director jumpsuit
loadout-group-research-director-backpack = Research Director backpack
loadout-group-research-director-outerclothing = Research Director outer clothing
loadout-group-research-director-shoes = Research Director 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-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-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-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

@@ -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

View File

@@ -85,6 +85,10 @@
0: Alive
450: Critical
500: Dead
- type: SlowOnDamage
speedModifierThresholds:
250: 0.7
400: 0.5
- type: Metabolizer
solutionOnBody: false
updateInterval: 0.25

View File

@@ -60,6 +60,21 @@
damage: 400
behaviors:
- !type:GibBehavior { }
- trigger:
!type:DamageTypeTrigger
damageType: Heat
damage: 1500
behaviors:
- !type:SpawnEntitiesBehavior
spawnInContainer: true
spawn:
Ash:
min: 1
max: 1
- !type:BurnBodyBehavior { }
- !type:PlaySoundBehavior
sound:
collection: MeatLaserImpact
- type: RadiationReceiver
- type: Stamina
- type: MobState

View File

@@ -229,7 +229,7 @@
suffix: Security/Lawyer, Locked
components:
- type: AccessReader
access: [["Security", "Lawyer"]]
access: [["Security"], ["Lawyer"]]
- type: entity
parent: DoorElectronics
@@ -261,7 +261,7 @@
suffix: Vault, Locked
components:
- type: AccessReader
access: [["Security", "Command"]]
access: [["Security"], ["Command"]]
- type: entity
parent: DoorElectronics

View File

@@ -1310,9 +1310,7 @@
components:
- type: Sprite
sprite: Objects/Fun/rubber_hammer.rsi
layers:
- state: icon
shader: unshaded
state: icon
- type: WeaponRandom
damageBonus:
types:

View File

@@ -802,7 +802,7 @@
- type: Unremoveable
- type: entity
parent: IDCardStandard
parent: EngineeringIDCard
id: SeniorEngineerIDCard
name: senior engineer ID card
components:
@@ -812,7 +812,7 @@
- state: idseniorengineer
- type: entity
parent: IDCardStandard
parent: ResearchIDCard
id: SeniorResearcherIDCard
name: senior researcher ID card
components:
@@ -822,7 +822,7 @@
- state: idseniorresearcher
- type: entity
parent: IDCardStandard
parent: MedicalIDCard
id: SeniorPhysicianIDCard
name: senior physician ID card
components:
@@ -832,7 +832,7 @@
- state: idseniorphysician
- type: entity
parent: IDCardStandard
parent: SecurityIDCard
id: SeniorOfficerIDCard
name: senior officer ID card
components:

View File

@@ -351,6 +351,8 @@
damage:
types:
Piercing: 15
soundHit:
path: /Audio/Weapons/bladeslice.ogg
- type: Tool
qualities:
- Screwing

View File

@@ -50,7 +50,7 @@
Blunt: 1
Heat: 2
- type: IgniteOnCollide
fireStacks: 3
fireStacks: 1
count: 10
- type: TimedDespawn
lifetime: 0.25

View File

@@ -1041,6 +1041,15 @@
containers:
board: [ DoorElectronicsResearchDirector ]
- type: entity
parent: AirlockMaintCommandLocked
id: AirlockMaintQuartermasterLocked
suffix: Quartermaster, Locked
components:
- type: ContainerFill
containers:
board: [ DoorElectronicsQuartermaster ]
- type: entity
parent: AirlockMaint
id: AirlockMaintArmoryLocked

View File

@@ -169,7 +169,7 @@
sprite: Structures/Walls/meat.rsi
- type: Construction
graph: Girder
node: bananiumWall
node: meatWall
- type: Destructible
thresholds:
- trigger:

View File

@@ -69,7 +69,7 @@
noSpawn: true
components:
- type: GameRule
minPlayers: 35
minPlayers: 20
- type: NukeopsRule
faction: Syndicate

View File

@@ -0,0 +1,76 @@
# Head
- type: loadout
id: CargoTechnicianHead
equipment: CargoTechnicianHead
- type: startingGear
id: CargoTechnicianHead
equipment:
head: ClothingHeadHatCargosoft
# Jumpsuit
- type: loadout
id: CargoTechnicianJumpsuit
equipment: CargoTechnicianJumpsuit
- type: startingGear
id: CargoTechnicianJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitCargo
- type: loadout
id: CargoTechnicianJumpskirt
equipment: CargoTechnicianJumpskirt
- type: startingGear
id: CargoTechnicianJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtCargo
# Back
- type: loadout
id: CargoTechnicianBackpack
equipment: CargoTechnicianBackpack
- type: startingGear
id: CargoTechnicianBackpack
equipment:
back: ClothingBackpackCargoFilled
- type: loadout
id: CargoTechnicianSatchel
equipment: CargoTechnicianSatchel
- type: startingGear
id: CargoTechnicianSatchel
equipment:
back: ClothingBackpackSatchelCargoFilled
- type: loadout
id: CargoTechnicianDuffel
equipment: CargoTechnicianDuffel
- type: startingGear
id: CargoTechnicianDuffel
equipment:
back: ClothingBackpackDuffelCargoFilled
# OuterClothing
- type: loadout
id: CargoTechnicianWintercoat
equipment: CargoTechnicianWintercoat
- type: startingGear
id: CargoTechnicianWintercoat
equipment:
outerClothing: ClothingOuterWinterCargo
# Shoes
- type: loadout
id: CargoWinterBoots
equipment: CargoWinterBoots
- type: startingGear
id: CargoWinterBoots
equipment:
shoes: ClothingShoesBootsWinterCargo

View File

@@ -0,0 +1,121 @@
# Jumpsuit
- type: loadout
id: QuartermasterJumpsuit
equipment: QuartermasterJumpsuit
- type: startingGear
id: QuartermasterJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitQM
- type: loadout
id: QuartermasterJumpskirt
equipment: QuartermasterJumpskirt
- type: startingGear
id: QuartermasterJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtQM
- type: loadout
id: QuartermasterTurtleneck
equipment: QuartermasterTurtleneck
- type: startingGear
id: QuartermasterTurtleneck
equipment:
jumpsuit: ClothingUniformJumpsuitQMTurtleneck
- type: loadout
id: QuartermasterTurtleneckSkirt
equipment: QuartermasterTurtleneckSkirt
- type: startingGear
id: QuartermasterTurtleneckSkirt
equipment:
jumpsuit: ClothingUniformJumpskirtQMTurtleneck
- type: loadout
id: QuartermasterFormalSuit
equipment: QuartermasterFormalSuit
- type: startingGear
id: QuartermasterFormalSuit
equipment:
jumpsuit: ClothingUniformJumpsuitQMFormal
# Head
- type: loadout
id: QuartermasterHead
equipment: QuartermasterHead
- type: startingGear
id: QuartermasterHead
equipment:
head: ClothingHeadHatQMsoft
- type: loadout
id: QuartermasterBeret
equipment: QuartermasterBeret
- type: startingGear
id: QuartermasterBeret
equipment:
head: ClothingHeadHatBeretQM
# Neck
- type: loadout
id: QuartermasterCloak
equipment: QuartermasterCloak
- type: startingGear
id: QuartermasterCloak
equipment:
neck: ClothingNeckCloakQm
- type: loadout
id: QuartermasterMantle
equipment: QuartermasterMantle
- type: startingGear
id: QuartermasterMantle
equipment:
neck: ClothingNeckMantleQM
# Back
- type: loadout
id: QuartermasterBackpack
equipment: QuartermasterBackpack
- type: startingGear
id: QuartermasterBackpack
equipment:
back: ClothingBackpackQuartermasterFilled
- type: loadout
id: QuartermasterSatchel
equipment: QuartermasterSatchel
- type: startingGear
id: QuartermasterSatchel
equipment:
back: ClothingBackpackSatchelQuartermasterFilled
- type: loadout
id: QuartermasterDuffel
equipment: QuartermasterDuffel
- type: startingGear
id: QuartermasterDuffel
equipment:
back: ClothingBackpackDuffelQuartermasterFilled
# OuterClothing
- type: loadout
id: QuartermasterWintercoat
equipment: QuartermasterWintercoat
- type: startingGear
id: QuartermasterWintercoat
equipment:
outerClothing: ClothingOuterWinterQM

View File

@@ -0,0 +1,47 @@
# Back
- type: loadout
id: SalvageSpecialistBackpack
equipment: SalvageSpecialistBackpack
- type: startingGear
id: SalvageSpecialistBackpack
equipment:
back: ClothingBackpackSalvageFilled
- type: loadout
id: SalvageSpecialistSatchel
equipment: SalvageSpecialistSatchel
- type: startingGear
id: SalvageSpecialistSatchel
equipment:
back: ClothingBackpackSatchelSalvageFilled
- type: loadout
id: SalvageSpecialistDuffel
equipment: SalvageSpecialistDuffel
- type: startingGear
id: SalvageSpecialistDuffel
equipment:
back: ClothingBackpackDuffelSalvageFilled
# OuterClothing
- type: loadout
id: SalvageSpecialistWintercoat
equipment: SalvageSpecialistWintercoat
- type: startingGear
id: SalvageSpecialistWintercoat
equipment:
outerClothing: ClothingOuterWinterMiner
# Shoes
- type: loadout
id: SalvageBoots
equipment: SalvageBoots
- type: startingGear
id: SalvageBoots
equipment:
shoes: ClothingShoesBootsSalvage

View File

@@ -0,0 +1,74 @@
# Head
- type: loadout
id: BartenderHead
equipment: BartenderHead
- type: startingGear
id: BartenderHead
equipment:
head: ClothingHeadHatTophat
- type: loadout
id: BartenderBowler
equipment: BartenderBowler
- type: startingGear
id: BartenderBowler
equipment:
head: ClothingHeadHatBowlerHat
# Jumpsuit
- type: loadout
id: BartenderJumpsuit
equipment: BartenderJumpsuit
- type: startingGear
id: BartenderJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitBartender
- type: loadout
id: BartenderJumpskirt
equipment: BartenderJumpskirt
- type: startingGear
id: BartenderJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtBartender
- type: loadout
id: BartenderJumpsuitPurple
equipment: BartenderJumpsuitPurple
- type: startingGear
id: BartenderJumpsuitPurple
equipment:
jumpsuit: ClothingUniformJumpsuitBartenderPurple
# Outer clothing
- type: loadout
id: BartenderApron
equipment: BartenderApron
- type: startingGear
id: BartenderApron
equipment:
outerClothing: ClothingOuterApronBar
- type: loadout
id: BartenderVest
equipment: BartenderVest
- type: startingGear
id: BartenderVest
equipment:
outerClothing: ClothingOuterVest
- type: loadout
id: BartenderWintercoat
equipment: BartenderWintercoat
- type: startingGear
id: BartenderWintercoat
equipment:
outerClothing: ClothingOuterWinterBar

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