mirror of
https://github.com/space-wizards/space-station-14.git
synced 2026-02-14 19:29:53 +01:00
Visual nubody (humanoid appearance refactor) (#42476)
* initial visual nubody * oops overlay * im so pheeming rn * conversion... * tests * comeback of the underwear * oops eyes * blabbl * zeds * yaml linted * search and visible count constraints * reordering * preserve previously selected markings colors * fix test * some ui niceties * ordering * make DB changes backwards-compatible/downgrade-friendly * fix things again * fix migration * vulpkanin markings limit increase * wrapping * code cleanup and more code cleanup and more code cleanup and more code cleanup and * fix slop ports * better sampling API * make filter work + use the method i made for its intended purpose * fix test fails real quick * magic mirror cleanup, remove TODO * don't 0-init the organ profile data * remove deltastates --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
27bb73ded4
commit
8cf744ec55
@@ -25,8 +25,8 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
|
||||
var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap);
|
||||
|
||||
if (TryComp<HumanoidAppearanceComponent>(ent, out var humanoidAppearance) &&
|
||||
ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite))
|
||||
if (TryComp<HumanoidProfileComponent>(ent, out var humanoid) &&
|
||||
ent.Comp.SpeciesSprites.TryGetValue(humanoid.Species, out var speciesSprite))
|
||||
{
|
||||
_sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite);
|
||||
}
|
||||
|
||||
261
Content.Client/Body/VisualBodySystem.cs
Normal file
261
Content.Client/Body/VisualBodySystem.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Body;
|
||||
|
||||
public sealed class VisualBodySystem : SharedVisualBodySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly MarkingManager _marking = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<VisualOrganComponent, OrganGotInsertedEvent>(OnOrganGotInserted);
|
||||
SubscribeLocalEvent<VisualOrganComponent, OrganGotRemovedEvent>(OnOrganGotRemoved);
|
||||
SubscribeLocalEvent<VisualOrganComponent, AfterAutoHandleStateEvent>(OnOrganState);
|
||||
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotInsertedEvent>(OnMarkingsGotInserted);
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotRemovedEvent>(OnMarkingsGotRemoved);
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, AfterAutoHandleStateEvent>(OnMarkingsState);
|
||||
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent>>(OnMarkingsChangedVisibility);
|
||||
|
||||
Subs.CVar(_cfg, CCVars.AccessibilityClientCensorNudity, OnCensorshipChanged, true);
|
||||
Subs.CVar(_cfg, CCVars.AccessibilityServerCensorNudity, OnCensorshipChanged, true);
|
||||
}
|
||||
|
||||
private void OnCensorshipChanged(bool value)
|
||||
{
|
||||
var query = AllEntityQuery<OrganComponent, VisualOrganMarkingsComponent>();
|
||||
while (query.MoveNext(out var ent, out var organComp, out var markingsComp))
|
||||
{
|
||||
if (organComp.Body is not { } body)
|
||||
continue;
|
||||
|
||||
RemoveMarkings((ent, markingsComp), body);
|
||||
ApplyMarkings((ent, markingsComp), body);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOrganGotInserted(Entity<VisualOrganComponent> ent, ref OrganGotInsertedEvent args)
|
||||
{
|
||||
ApplyVisual(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnOrganGotRemoved(Entity<VisualOrganComponent> ent, ref OrganGotRemovedEvent args)
|
||||
{
|
||||
RemoveVisual(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnOrganState(Entity<VisualOrganComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
ApplyVisual(ent, body);
|
||||
}
|
||||
|
||||
private void ApplyVisual(Entity<VisualOrganComponent> ent, EntityUid target)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true))
|
||||
return;
|
||||
|
||||
_sprite.LayerSetData(target, index, ent.Comp.Data);
|
||||
}
|
||||
|
||||
private void RemoveVisual(Entity<VisualOrganComponent> ent, EntityUid target)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true))
|
||||
return;
|
||||
|
||||
_sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid);
|
||||
}
|
||||
|
||||
private void OnMarkingsGotInserted(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotInsertedEvent args)
|
||||
{
|
||||
ApplyMarkings(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnMarkingsGotRemoved(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotRemovedEvent args)
|
||||
{
|
||||
RemoveMarkings(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnMarkingsState(Entity<VisualOrganMarkingsComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
RemoveMarkings(ent, body);
|
||||
ApplyMarkings(ent, body);
|
||||
}
|
||||
|
||||
protected override void SetOrganColor(Entity<VisualOrganComponent> ent, Color color)
|
||||
{
|
||||
base.SetOrganColor(ent, color);
|
||||
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
ApplyVisual(ent, body);
|
||||
}
|
||||
|
||||
protected override void SetOrganMarkings(Entity<VisualOrganMarkingsComponent> ent, Dictionary<HumanoidVisualLayers, List<Marking>> markings)
|
||||
{
|
||||
base.SetOrganMarkings(ent, markings);
|
||||
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
RemoveMarkings(ent, body);
|
||||
ApplyMarkings(ent, body);
|
||||
}
|
||||
|
||||
protected override void SetOrganAppearance(Entity<VisualOrganComponent> ent, PrototypeLayerData data)
|
||||
{
|
||||
base.SetOrganAppearance(ent, data);
|
||||
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
ApplyVisual(ent, body);
|
||||
}
|
||||
|
||||
private IEnumerable<Marking> AllMarkings(Entity<VisualOrganMarkingsComponent> ent)
|
||||
{
|
||||
foreach (var markings in ent.Comp.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markings)
|
||||
{
|
||||
yield return marking;
|
||||
}
|
||||
}
|
||||
|
||||
var censorNudity = _cfg.GetCVar(CCVars.AccessibilityClientCensorNudity) || _cfg.GetCVar(CCVars.AccessibilityServerCensorNudity);
|
||||
if (!censorNudity)
|
||||
yield break;
|
||||
|
||||
var group = _prototype.Index(ent.Comp.MarkingData.Group);
|
||||
foreach (var layer in ent.Comp.MarkingData.Layers)
|
||||
{
|
||||
if (!group.Limits.TryGetValue(layer, out var layerLimits))
|
||||
continue;
|
||||
|
||||
if (layerLimits.NudityDefault.Count < 1)
|
||||
continue;
|
||||
|
||||
var markings = ent.Comp.Markings.GetValueOrDefault(layer) ?? [];
|
||||
if (markings.Any(marking => _marking.TryGetMarking(marking, out var proto) && proto.BodyPart == layer))
|
||||
continue;
|
||||
|
||||
foreach (var marking in layerLimits.NudityDefault)
|
||||
{
|
||||
yield return new(marking, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
|
||||
{
|
||||
var applied = new List<Marking>();
|
||||
foreach (var marking in AllMarkings(ent))
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
if (!_sprite.LayerMapTryGet(target, proto.BodyPart, out var index, true))
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < proto.Sprites.Count; i++)
|
||||
{
|
||||
var sprite = proto.Sprites[i];
|
||||
|
||||
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{proto.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet(target, layerId, out _, false))
|
||||
{
|
||||
var layer = _sprite.AddLayer(target, sprite, index + i + 1);
|
||||
_sprite.LayerMapSet(target, layerId, layer);
|
||||
_sprite.LayerSetSprite(target, layerId, rsi);
|
||||
}
|
||||
|
||||
if (marking.MarkingColors is not null && i < marking.MarkingColors.Count)
|
||||
_sprite.LayerSetColor(target, layerId, marking.MarkingColors[i]);
|
||||
else
|
||||
_sprite.LayerSetColor(target, layerId, Color.White);
|
||||
}
|
||||
|
||||
applied.Add(marking);
|
||||
}
|
||||
ent.Comp.AppliedMarkings = applied;
|
||||
}
|
||||
|
||||
private void RemoveMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
|
||||
{
|
||||
foreach (var marking in ent.Comp.AppliedMarkings)
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
foreach (var sprite in proto.Sprites)
|
||||
{
|
||||
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{proto.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet(target, layerId, out var index, false))
|
||||
continue;
|
||||
|
||||
_sprite.LayerMapRemove(target, layerId);
|
||||
_sprite.RemoveLayer(target, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMarkingsChangedVisibility(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent> args)
|
||||
{
|
||||
foreach (var markings in ent.Comp.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markings)
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
if (proto.BodyPart != args.Args.Layer)
|
||||
continue;
|
||||
|
||||
foreach (var sprite in proto.Sprites)
|
||||
{
|
||||
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{proto.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet(args.Body.Owner, layerId, out var index, true))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible(args.Body.Owner, index, args.Args.Visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +271,7 @@ public sealed class ClientClothingSystem : ClothingSystem
|
||||
// Select displacement maps
|
||||
var displacementData = inventory.Displacements.GetValueOrDefault(slot); //Default unsexed map
|
||||
|
||||
var equipeeSex = CompOrNull<HumanoidAppearanceComponent>(equipee)?.Sex;
|
||||
var equipeeSex = CompOrNull<HumanoidProfileComponent>(equipee)?.Sex;
|
||||
if (equipeeSex != null)
|
||||
{
|
||||
switch (equipeeSex)
|
||||
|
||||
@@ -79,9 +79,9 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
NameLabel.SetMessage(name);
|
||||
|
||||
SpeciesLabel.Text =
|
||||
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
|
||||
out var humanoidAppearanceComponent)
|
||||
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
|
||||
_entityManager.TryGetComponent<HumanoidProfileComponent>(target.Value,
|
||||
out var humanoidComponent)
|
||||
? Loc.GetString(_prototypes.Index(humanoidComponent.Species).Name)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
|
||||
|
||||
// Basic Diagnostic
|
||||
|
||||
74
Content.Client/Humanoid/HideableHumanoidLayersSystem.cs
Normal file
74
Content.Client/Humanoid/HideableHumanoidLayersSystem.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HideableHumanoidLayersComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<HideableHumanoidLayersComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnComponentInit(Entity<HideableHumanoidLayersComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
UpdateSprite(ent);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<HideableHumanoidLayersComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateSprite(ent);
|
||||
}
|
||||
|
||||
public override void SetLayerVisibility(
|
||||
Entity<HideableHumanoidLayersComponent?> ent,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags source)
|
||||
{
|
||||
base.SetLayerVisibility(ent, layer, visible, source);
|
||||
|
||||
if (Resolve(ent, ref ent.Comp))
|
||||
UpdateSprite((ent, ent.Comp));
|
||||
}
|
||||
|
||||
private void UpdateSprite(Entity<HideableHumanoidLayersComponent> ent)
|
||||
{
|
||||
foreach (var item in ent.Comp.LastHiddenLayers)
|
||||
{
|
||||
if (ent.Comp.HiddenLayers.ContainsKey(item))
|
||||
continue;
|
||||
|
||||
var evt = new HumanoidLayerVisibilityChangedEvent(item, true);
|
||||
RaiseLocalEvent(ent, ref evt);
|
||||
|
||||
if (!_sprite.LayerMapTryGet(ent.Owner, item, out var index, true))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible(ent.Owner, index, true);
|
||||
}
|
||||
|
||||
foreach (var item in ent.Comp.HiddenLayers.Keys)
|
||||
{
|
||||
if (ent.Comp.LastHiddenLayers.Contains(item))
|
||||
continue;
|
||||
|
||||
var evt = new HumanoidLayerVisibilityChangedEvent(item, false);
|
||||
RaiseLocalEvent(ent, ref evt);
|
||||
|
||||
if (!_sprite.LayerMapTryGet(ent.Owner, item, out var index, true))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible(ent.Owner, index, false);
|
||||
}
|
||||
|
||||
ent.Comp.LastHiddenLayers.Clear();
|
||||
ent.Comp.LastHiddenLayers.UnionWith(ent.Comp.HiddenLayers.Keys);
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly DisplacementMapSystem _displacement = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
|
||||
Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateSprite((uid, component, Comp<SpriteComponent>(uid)));
|
||||
}
|
||||
|
||||
private void OnCvarChanged(bool value)
|
||||
{
|
||||
var humanoidQuery = AllEntityQuery<HumanoidAppearanceComponent, SpriteComponent>();
|
||||
while (humanoidQuery.MoveNext(out var uid, out var humanoidComp, out var spriteComp))
|
||||
{
|
||||
UpdateSprite((uid, humanoidComp, spriteComp));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSprite(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
UpdateLayers(entity);
|
||||
ApplyMarkingSet(entity);
|
||||
|
||||
var humanoidAppearance = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
sprite[_sprite.LayerMapReserve((entity.Owner, sprite), HumanoidVisualLayers.Eyes)].Color = humanoidAppearance.EyeColor;
|
||||
}
|
||||
|
||||
private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
|
||||
=> humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
|
||||
|
||||
private void UpdateLayers(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var component = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
var oldLayers = new HashSet<HumanoidVisualLayers>(component.BaseLayers.Keys);
|
||||
component.BaseLayers.Clear();
|
||||
|
||||
// add default species layers
|
||||
var speciesProto = _prototypeManager.Index(component.Species);
|
||||
var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet);
|
||||
foreach (var (key, id) in baseSprites.Sprites)
|
||||
{
|
||||
oldLayers.Remove(key);
|
||||
if (!component.CustomBaseLayers.ContainsKey(key))
|
||||
SetLayerData(entity, key, id, sexMorph: true);
|
||||
}
|
||||
|
||||
// add custom layers
|
||||
foreach (var (key, info) in component.CustomBaseLayers)
|
||||
{
|
||||
oldLayers.Remove(key);
|
||||
SetLayerData(entity, key, info.Id, sexMorph: false, color: info.Color);
|
||||
}
|
||||
|
||||
// hide old layers
|
||||
// TODO maybe just remove them altogether?
|
||||
foreach (var key in oldLayers)
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((entity.Owner, sprite), key, out var index, false))
|
||||
sprite[index].Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLayerData(
|
||||
Entity<HumanoidAppearanceComponent, SpriteComponent> entity,
|
||||
HumanoidVisualLayers key,
|
||||
string? protoId,
|
||||
bool sexMorph = false,
|
||||
Color? color = null)
|
||||
{
|
||||
var component = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
var layerIndex = _sprite.LayerMapReserve((entity.Owner, sprite), key);
|
||||
var layer = sprite[layerIndex];
|
||||
layer.Visible = !IsHidden(component, key);
|
||||
|
||||
if (color != null)
|
||||
layer.Color = color.Value;
|
||||
|
||||
if (protoId == null)
|
||||
return;
|
||||
|
||||
if (sexMorph)
|
||||
protoId = HumanoidVisualLayersExtension.GetSexMorph(key, component.Sex, protoId);
|
||||
|
||||
var proto = _prototypeManager.Index<HumanoidSpeciesSpriteLayer>(protoId);
|
||||
component.BaseLayers[key] = proto;
|
||||
|
||||
if (proto.MatchSkin)
|
||||
layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha);
|
||||
|
||||
if (proto.BaseSprite != null)
|
||||
_sprite.LayerSetSprite((entity.Owner, sprite), layerIndex, proto.BaseSprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a profile directly into a humanoid.
|
||||
/// </summary>
|
||||
/// <param name="uid">The humanoid entity's UID</param>
|
||||
/// <param name="profile">The profile to load.</param>
|
||||
/// <param name="humanoid">The humanoid entity's humanoid component.</param>
|
||||
/// <remarks>
|
||||
/// This should not be used if the entity is owned by the server. The server will otherwise
|
||||
/// override this with the appearance data it sends over.
|
||||
/// </remarks>
|
||||
public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref humanoid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var customBaseLayers = new Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo>();
|
||||
|
||||
var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(profile.Species);
|
||||
var markings = new MarkingSet(speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
|
||||
|
||||
// Add markings that doesn't need coloring. We store them until we add all other markings that doesn't need it.
|
||||
var markingFColored = new Dictionary<Marking, MarkingPrototype>();
|
||||
foreach (var marking in profile.Appearance.Markings)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
if (!prototype.ForcedColoring)
|
||||
{
|
||||
markings.AddBack(prototype.MarkingCategory, marking);
|
||||
}
|
||||
else
|
||||
{
|
||||
markingFColored.Add(marking, prototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// legacy: remove in the future?
|
||||
//markings.RemoveCategory(MarkingCategories.Hair);
|
||||
//markings.RemoveCategory(MarkingCategories.FacialHair);
|
||||
|
||||
// We need to ensure hair before applying it or coloring can try depend on markings that can be invalid
|
||||
var hairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.Hair, out var hairAlpha, _prototypeManager)
|
||||
? profile.Appearance.SkinColor.WithAlpha(hairAlpha)
|
||||
: profile.Appearance.HairColor;
|
||||
var hair = new Marking(profile.Appearance.HairStyleId,
|
||||
new[] { hairColor });
|
||||
|
||||
var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, out var facialHairAlpha, _prototypeManager)
|
||||
? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha)
|
||||
: profile.Appearance.FacialHairColor;
|
||||
var facialHair = new Marking(profile.Appearance.FacialHairStyleId,
|
||||
new[] { facialHairColor });
|
||||
|
||||
if (_markingManager.CanBeApplied(profile.Species, profile.Sex, hair, _prototypeManager))
|
||||
{
|
||||
markings.AddBack(MarkingCategories.Hair, hair);
|
||||
}
|
||||
if (_markingManager.CanBeApplied(profile.Species, profile.Sex, facialHair, _prototypeManager))
|
||||
{
|
||||
markings.AddBack(MarkingCategories.FacialHair, facialHair);
|
||||
}
|
||||
|
||||
// Finally adding marking with forced colors
|
||||
foreach (var (marking, prototype) in markingFColored)
|
||||
{
|
||||
var markingColors = MarkingColoring.GetMarkingLayerColors(
|
||||
prototype,
|
||||
profile.Appearance.SkinColor,
|
||||
profile.Appearance.EyeColor,
|
||||
markings
|
||||
);
|
||||
markings.AddBack(prototype.MarkingCategory, new Marking(marking.MarkingId, markingColors));
|
||||
}
|
||||
|
||||
markings.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager);
|
||||
markings.EnsureSexes(profile.Sex, _markingManager);
|
||||
markings.EnsureDefault(
|
||||
profile.Appearance.SkinColor,
|
||||
profile.Appearance.EyeColor,
|
||||
_markingManager);
|
||||
|
||||
DebugTools.Assert(IsClientSide(uid));
|
||||
|
||||
humanoid.MarkingSet = markings;
|
||||
humanoid.PermanentlyHidden = new HashSet<HumanoidVisualLayers>();
|
||||
humanoid.HiddenLayers = new Dictionary<HumanoidVisualLayers, SlotFlags>();
|
||||
humanoid.CustomBaseLayers = customBaseLayers;
|
||||
humanoid.Sex = profile.Sex;
|
||||
humanoid.Gender = profile.Gender;
|
||||
humanoid.Age = profile.Age;
|
||||
humanoid.Species = profile.Species;
|
||||
humanoid.SkinColor = profile.Appearance.SkinColor;
|
||||
humanoid.EyeColor = profile.Appearance.EyeColor;
|
||||
|
||||
UpdateSprite((uid, humanoid, Comp<SpriteComponent>(uid)));
|
||||
}
|
||||
|
||||
private void ApplyMarkingSet(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
// I am lazy and I CBF resolving the previous mess, so I'm just going to nuke the markings.
|
||||
// Really, markings should probably be a separate component altogether.
|
||||
ClearAllMarkings(entity);
|
||||
|
||||
var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
|
||||
_configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
|
||||
// The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
|
||||
var applyUndergarmentTop = censorNudity;
|
||||
var applyUndergarmentBottom = censorNudity;
|
||||
|
||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
|
||||
{
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, entity);
|
||||
if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
|
||||
applyUndergarmentTop = false;
|
||||
else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
|
||||
applyUndergarmentBottom = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
|
||||
|
||||
AddUndergarments(entity, applyUndergarmentTop, applyUndergarmentBottom);
|
||||
}
|
||||
|
||||
private void ClearAllMarkings(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
foreach (var markingList in humanoid.ClientOldMarkings.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
RemoveMarking(marking, (entity, sprite));
|
||||
}
|
||||
}
|
||||
|
||||
humanoid.ClientOldMarkings.Clear();
|
||||
|
||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
RemoveMarking(marking, (entity, sprite));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveMarking(Marking marking, Entity<SpriteComponent> spriteEnt)
|
||||
{
|
||||
if (!_markingManager.TryGetMarking(marking, out var prototype))
|
||||
return;
|
||||
|
||||
foreach (var sprite in prototype.Sprites)
|
||||
{
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{marking.MarkingId}-{rsi.RsiState}";
|
||||
if (!_sprite.LayerMapTryGet(spriteEnt.AsNullable(), layerId, out var index, false))
|
||||
continue;
|
||||
|
||||
_sprite.LayerMapRemove(spriteEnt.AsNullable(), layerId);
|
||||
_sprite.RemoveLayer(spriteEnt.AsNullable(), index);
|
||||
|
||||
// If this marking is one that can be displaced, we need to remove the displacement as well; otherwise
|
||||
// altering a marking at runtime can lead to the renderer falling over.
|
||||
// The Vulps must be shaved.
|
||||
// (https://github.com/space-wizards/space-station-14/issues/40135).
|
||||
if (prototype.CanBeDisplaced)
|
||||
_displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUndergarments(Entity<HumanoidAppearanceComponent, SpriteComponent> entity, bool undergarmentTop, bool undergarmentBottom)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
|
||||
if (undergarmentTop && humanoid.UndergarmentTop != null)
|
||||
{
|
||||
var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
// Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
|
||||
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking> { marking });
|
||||
ApplyMarking(prototype, null, true, entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (undergarmentBottom && humanoid.UndergarmentBottom != null)
|
||||
{
|
||||
var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking> { marking });
|
||||
ApplyMarking(prototype, null, true, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMarking(MarkingPrototype markingPrototype,
|
||||
IReadOnlyList<Color>? colors,
|
||||
bool visible,
|
||||
Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
if (!_sprite.LayerMapTryGet((entity.Owner, sprite), markingPrototype.BodyPart, out var targetLayer, false))
|
||||
return;
|
||||
|
||||
visible &= !IsHidden(humanoid, markingPrototype.BodyPart);
|
||||
visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting)
|
||||
&& setting.AllowsMarkings;
|
||||
|
||||
for (var j = 0; j < markingPrototype.Sprites.Count; j++)
|
||||
{
|
||||
var markingSprite = markingPrototype.Sprites[j];
|
||||
|
||||
if (markingSprite is not SpriteSpecifier.Rsi rsi)
|
||||
return;
|
||||
|
||||
var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet((entity.Owner, sprite), layerId, out _, false))
|
||||
{
|
||||
var layer = _sprite.AddLayer((entity.Owner, sprite), markingSprite, targetLayer + j + 1);
|
||||
_sprite.LayerMapSet((entity.Owner, sprite), layerId, layer);
|
||||
_sprite.LayerSetSprite((entity.Owner, sprite), layerId, rsi);
|
||||
}
|
||||
|
||||
_sprite.LayerSetVisible((entity.Owner, sprite), layerId, visible);
|
||||
|
||||
if (!visible || setting == null) // this is kinda implied
|
||||
continue;
|
||||
|
||||
// Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
|
||||
// and we need to check the index is correct.
|
||||
// So if that happens just default to white?
|
||||
if (colors != null && j < colors.Count)
|
||||
_sprite.LayerSetColor((entity.Owner, sprite), layerId, colors[j]);
|
||||
else
|
||||
_sprite.LayerSetColor((entity.Owner, sprite), layerId, Color.White);
|
||||
|
||||
if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced)
|
||||
_displacement.TryAddDisplacement(displacementData, (entity.Owner, sprite), targetLayer + j + 1, layerId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, bool verify = true, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (!Resolve(uid, ref humanoid) || humanoid.SkinColor == skinColor)
|
||||
return;
|
||||
|
||||
base.SetSkinColor(uid, skinColor, false, verify, humanoid);
|
||||
|
||||
if (!TryComp(uid, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
foreach (var (layer, spriteInfo) in humanoid.BaseLayers)
|
||||
{
|
||||
if (!spriteInfo.MatchSkin)
|
||||
continue;
|
||||
|
||||
var index = _sprite.LayerMapReserve((uid, sprite), layer);
|
||||
sprite[index].Color = skinColor.WithAlpha(spriteInfo.LayerAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayerVisibility(
|
||||
Entity<HumanoidAppearanceComponent> ent,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags? slot,
|
||||
ref bool dirty)
|
||||
{
|
||||
base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
|
||||
|
||||
var sprite = Comp<SpriteComponent>(ent);
|
||||
if (!_sprite.LayerMapTryGet((ent.Owner, sprite), layer, out var index, false))
|
||||
{
|
||||
if (!visible)
|
||||
return;
|
||||
index = _sprite.LayerMapReserve((ent.Owner, sprite), layer);
|
||||
}
|
||||
|
||||
var spriteLayer = sprite[index];
|
||||
if (spriteLayer.Visible == visible)
|
||||
return;
|
||||
|
||||
spriteLayer.Visible = visible;
|
||||
|
||||
// I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
|
||||
// Just a week away...
|
||||
|
||||
foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, (ent, ent.Comp, sprite));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
@@ -13,6 +12,8 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa
|
||||
[ViewVariables]
|
||||
private HumanoidMarkingModifierWindow? _window;
|
||||
|
||||
private readonly MarkingsViewModel _markingsModel = new();
|
||||
|
||||
public HumanoidMarkingModifierBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
@@ -22,11 +23,11 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindowCenteredLeft<HumanoidMarkingModifierWindow>();
|
||||
_window.OnMarkingAdded += SendMarkingSet;
|
||||
_window.OnMarkingRemoved += SendMarkingSet;
|
||||
_window.OnMarkingColorChange += SendMarkingSetNoResend;
|
||||
_window.OnMarkingRankChange += SendMarkingSet;
|
||||
_window.OnLayerInfoModified += SendBaseLayer;
|
||||
_window.MarkingPickerWidget.SetModel(_markingsModel);
|
||||
_window.RespectLimits.OnPressed += args => _markingsModel.EnforceLimits = args.Button.Pressed;
|
||||
_window.RespectGroupSex.OnPressed += args => _markingsModel.EnforceGroupAndSexRestrictions = args.Button.Pressed;
|
||||
|
||||
_markingsModel.MarkingsChanged += (_, _) => SendMarkingSet();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
@@ -34,26 +35,16 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa
|
||||
base.UpdateState(state);
|
||||
|
||||
if (_window == null || state is not HumanoidMarkingModifierState cast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_window.SetState(cast.MarkingSet, cast.Species, cast.Sex, cast.SkinColor, cast.CustomBaseLayers);
|
||||
_markingsModel.OrganData = cast.OrganData;
|
||||
_markingsModel.OrganProfileData = cast.OrganProfileData;
|
||||
_markingsModel.Markings = cast.Markings;
|
||||
}
|
||||
|
||||
private void SendMarkingSet(MarkingSet set)
|
||||
private void SendMarkingSet()
|
||||
{
|
||||
SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, true));
|
||||
}
|
||||
|
||||
private void SendMarkingSetNoResend(MarkingSet set)
|
||||
{
|
||||
SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, false));
|
||||
}
|
||||
|
||||
private void SendBaseLayer(HumanoidVisualLayers layer, CustomBaseLayerInfo? info)
|
||||
{
|
||||
SendMessage(new HumanoidMarkingModifierBaseLayersSetMessage(layer, info, true));
|
||||
SendMessage(new HumanoidMarkingModifierMarkingSetMessage(_markingsModel.Markings));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:humanoid="clr-namespace:Content.Client.Humanoid">
|
||||
<ScrollContainer MinHeight="500" MinWidth="700">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<humanoid:MarkingPicker Name="MarkingPickerWidget" />
|
||||
<BoxContainer>
|
||||
<CheckBox Name="MarkingForced" Text="{Loc humanoid-marking-modifier-force}" Pressed="True" />
|
||||
<CheckBox Name="MarkingIgnoreSpecies" Text="{Loc humanoid-marking-modifier-ignore-species}" Pressed="True" />
|
||||
</BoxContainer>
|
||||
<Collapsible HorizontalExpand="True">
|
||||
<CollapsibleHeading Title="{Loc humanoid-marking-modifier-base-layers}" />
|
||||
<CollapsibleBody HorizontalExpand="True">
|
||||
<BoxContainer Name="BaseLayersContainer" Orientation="Vertical" HorizontalExpand="True" />
|
||||
</CollapsibleBody>
|
||||
</Collapsible>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" MinHeight="500" MinWidth="700">
|
||||
<BoxContainer>
|
||||
<CheckBox Name="RespectLimits" Text="{Loc humanoid-marking-modifier-respect-limits}" Pressed="True" Access="Public" />
|
||||
<CheckBox Name="RespectGroupSex" Text="{Loc humanoid-marking-modifier-respect-group-sex}" Pressed="True" Access="Public" />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<humanoid:MarkingPicker Name="MarkingPickerWidget" Access="Public" HorizontalExpand="True" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -14,147 +14,8 @@ namespace Content.Client.Humanoid;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow
|
||||
{
|
||||
public Action<MarkingSet>? OnMarkingAdded;
|
||||
public Action<MarkingSet>? OnMarkingRemoved;
|
||||
public Action<MarkingSet>? OnMarkingColorChange;
|
||||
public Action<MarkingSet>? OnMarkingRankChange;
|
||||
public Action<HumanoidVisualLayers, CustomBaseLayerInfo?>? OnLayerInfoModified;
|
||||
private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private readonly Dictionary<HumanoidVisualLayers, HumanoidBaseLayerModifier> _modifiers = new();
|
||||
|
||||
public HumanoidMarkingModifierWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
foreach (var layer in Enum.GetValues<HumanoidVisualLayers>())
|
||||
{
|
||||
var modifier = new HumanoidBaseLayerModifier(layer);
|
||||
BaseLayersContainer.AddChild(modifier);
|
||||
_modifiers.Add(layer, modifier);
|
||||
|
||||
modifier.OnStateChanged += () => OnStateChanged(layer, modifier);
|
||||
}
|
||||
|
||||
MarkingPickerWidget.OnMarkingAdded += set => OnMarkingAdded!(set);
|
||||
MarkingPickerWidget.OnMarkingRemoved += set => OnMarkingRemoved!(set);
|
||||
MarkingPickerWidget.OnMarkingColorChange += set => OnMarkingColorChange!(set);
|
||||
MarkingPickerWidget.OnMarkingRankChange += set => OnMarkingRankChange!(set);
|
||||
MarkingForced.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed;
|
||||
MarkingIgnoreSpecies.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed;
|
||||
|
||||
MarkingPickerWidget.Forced = MarkingForced.Pressed;
|
||||
MarkingPickerWidget.IgnoreSpecies = MarkingForced.Pressed;
|
||||
}
|
||||
|
||||
private void OnStateChanged(HumanoidVisualLayers layer, HumanoidBaseLayerModifier modifier)
|
||||
{
|
||||
if (!modifier.Enabled)
|
||||
{
|
||||
OnLayerInfoModified?.Invoke(layer, null);
|
||||
return;
|
||||
}
|
||||
|
||||
string? state = _protoMan.HasIndex<HumanoidSpeciesSpriteLayer>(modifier.Text) ? modifier.Text : null;
|
||||
OnLayerInfoModified?.Invoke(layer, new CustomBaseLayerInfo(state, modifier.Color));
|
||||
}
|
||||
public void SetState(
|
||||
MarkingSet markings,
|
||||
string species,
|
||||
Sex sex,
|
||||
Color skinColor,
|
||||
Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> info
|
||||
)
|
||||
{
|
||||
foreach (var (layer, modifier) in _modifiers)
|
||||
{
|
||||
if (!info.TryGetValue(layer, out var layerInfo))
|
||||
{
|
||||
modifier.SetState(false, string.Empty, Color.White);
|
||||
continue;
|
||||
}
|
||||
|
||||
modifier.SetState(true, layerInfo.Id ?? string.Empty, layerInfo.Color ?? Color.White);
|
||||
}
|
||||
|
||||
var eyesColor = Color.White;
|
||||
if (info.TryGetValue(HumanoidVisualLayers.Eyes, out var eyes) && eyes.Color != null)
|
||||
{
|
||||
eyesColor = eyes.Color.Value;
|
||||
}
|
||||
|
||||
MarkingPickerWidget.SetData(markings, species, sex, skinColor, eyesColor);
|
||||
}
|
||||
|
||||
private sealed class HumanoidBaseLayerModifier : BoxContainer
|
||||
{
|
||||
private CheckBox _enable;
|
||||
private LineEdit _lineEdit;
|
||||
private ColorSelectorSliders _colorSliders;
|
||||
private BoxContainer _infoBox;
|
||||
|
||||
public bool Enabled => _enable.Pressed;
|
||||
public string Text => _lineEdit.Text;
|
||||
public Color Color => _colorSliders.Color;
|
||||
|
||||
public Action? OnStateChanged;
|
||||
|
||||
public HumanoidBaseLayerModifier(HumanoidVisualLayers layer)
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
var labelBox = new BoxContainer
|
||||
{
|
||||
MinWidth = 250,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
AddChild(labelBox);
|
||||
|
||||
labelBox.AddChild(new Label
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Text = layer.ToString()
|
||||
});
|
||||
_enable = new CheckBox
|
||||
{
|
||||
Text = Loc.GetString("humanoid-marking-modifier-enable"),
|
||||
HorizontalAlignment = HAlignment.Right
|
||||
};
|
||||
|
||||
labelBox.AddChild(_enable);
|
||||
_infoBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Visible = false
|
||||
};
|
||||
_enable.OnToggled += args =>
|
||||
{
|
||||
_infoBox.Visible = args.Pressed;
|
||||
OnStateChanged!();
|
||||
};
|
||||
|
||||
var lineEditBox = new BoxContainer { SeparationOverride = 4 };
|
||||
lineEditBox.AddChild(new Label { Text = Loc.GetString("humanoid-marking-modifier-prototype-id") });
|
||||
|
||||
// TODO: This line edit should really be an options / dropdown selector, not text.
|
||||
_lineEdit = new() { MinWidth = 200 };
|
||||
_lineEdit.OnTextEntered += args => OnStateChanged!();
|
||||
lineEditBox.AddChild(_lineEdit);
|
||||
_infoBox.AddChild(lineEditBox);
|
||||
|
||||
_colorSliders = new();
|
||||
_colorSliders.OnColorChanged += color => OnStateChanged!();
|
||||
_infoBox.AddChild(_colorSliders);
|
||||
AddChild(_infoBox);
|
||||
}
|
||||
|
||||
public void SetState(bool enabled, string state, Color color)
|
||||
{
|
||||
_enable.Pressed = enabled;
|
||||
_infoBox.Visible = enabled;
|
||||
_lineEdit.Text = state;
|
||||
_colorSliders.Color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
Content.Client/Humanoid/LayerMarkingItem.xaml
Normal file
23
Content.Client/Humanoid/LayerMarkingItem.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
MouseFilter="Pass">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
|
||||
<PanelContainer SetSize="64 64" HorizontalAlignment="Right" MouseFilter="Ignore">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<LayeredTextureRect TextureScale="2 2" Name="MarkingTexture" />
|
||||
</PanelContainer>
|
||||
|
||||
<Button Name="SelectButton" ToggleMode="True" HorizontalExpand="True" />
|
||||
|
||||
<Button Name="ColorsButton" ToggleMode="True" Visible="False">
|
||||
<TextureRect TexturePath="/Textures/Interface/palette.svg.png" HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="Scale" SetSize="24 24" />
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Name="ColorsContainer" Visible="False" Orientation="Vertical" />
|
||||
</BoxContainer>
|
||||
212
Content.Client/Humanoid/LayerMarkingItem.xaml.cs
Normal file
212
Content.Client/Humanoid/LayerMarkingItem.xaml.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Guidebook.Controls;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class LayerMarkingItem : BoxContainer, ISearchableControl
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
private readonly MarkingsViewModel _markingsModel;
|
||||
private readonly MarkingPrototype _markingPrototype;
|
||||
private readonly ProtoId<OrganCategoryPrototype> _organ;
|
||||
private readonly HumanoidVisualLayers _layer;
|
||||
private bool _interactive;
|
||||
|
||||
private List<ColorSelectorSliders>? _colorSliders;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, LayerMarkingItem>? Pressed;
|
||||
public event Action<GUIBoundKeyEventArgs, LayerMarkingItem>? Unpressed;
|
||||
public ProtoId<MarkingPrototype> MarkingId => _markingPrototype.ID;
|
||||
|
||||
public LayerMarkingItem(MarkingsViewModel model, ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, MarkingPrototype prototype, bool interactive)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entity.System<SpriteSystem>();
|
||||
|
||||
_markingsModel = model;
|
||||
_markingPrototype = prototype;
|
||||
_organ = organ;
|
||||
_layer = layer;
|
||||
_interactive = interactive;
|
||||
|
||||
UpdateData();
|
||||
UpdateSelection();
|
||||
|
||||
SelectButton.OnPressed += SelectButtonPressed;
|
||||
ColorsButton.OnPressed += ColorsButtonPressed;
|
||||
|
||||
OnKeyBindDown += OnPressed;
|
||||
OnKeyBindUp += OnUnpressed;
|
||||
|
||||
if (!interactive)
|
||||
{
|
||||
SelectButton.MouseFilter = Control.MouseFilterMode.Ignore;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_markingsModel.MarkingsReset += UpdateSelection;
|
||||
_markingsModel.MarkingsChanged += MarkingsChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_markingsModel.MarkingsReset -= UpdateSelection;
|
||||
_markingsModel.MarkingsChanged -= MarkingsChanged;
|
||||
}
|
||||
|
||||
private void MarkingsChanged(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer)
|
||||
{
|
||||
if (_organ != organ || _layer != layer)
|
||||
return;
|
||||
|
||||
UpdateSelection();
|
||||
}
|
||||
|
||||
private void UpdateData()
|
||||
{
|
||||
MarkingTexture.Textures = _markingPrototype.Sprites.Select(layer => _sprite.Frame0(layer)).ToList();
|
||||
SelectButton.Text = Loc.GetString($"marking-{_markingPrototype.ID}");
|
||||
}
|
||||
|
||||
private void UpdateSelection()
|
||||
{
|
||||
var selected = _markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID);
|
||||
SelectButton.Pressed = selected && _interactive;
|
||||
ColorsButton.Visible = selected && _interactive && _markingsModel.IsMarkingColorCustomizable(_organ, _layer, _markingPrototype.ID);
|
||||
|
||||
if (!selected || !_interactive)
|
||||
{
|
||||
ColorsButton.Pressed = false;
|
||||
ColorsContainer.Visible = false;
|
||||
}
|
||||
|
||||
if (_markingsModel.TryGetMarking(_organ, _layer, _markingPrototype.ID) is { } marking &&
|
||||
_colorSliders is { } sliders)
|
||||
{
|
||||
for (var i = 0; i < _markingPrototype.Sprites.Count; i++)
|
||||
{
|
||||
sliders[i].Color = marking.MarkingColors[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (!_interactive)
|
||||
{
|
||||
SelectButton.Pressed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID))
|
||||
{
|
||||
if (!_markingsModel.TryDeselectMarking(_organ, _layer, _markingPrototype.ID))
|
||||
{
|
||||
SelectButton.Pressed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_markingsModel.TrySelectMarking(_organ, _layer, _markingPrototype.ID))
|
||||
{
|
||||
SelectButton.Pressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ColorsButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
ColorsContainer.Visible = ColorsButton.Pressed;
|
||||
|
||||
if (_colorSliders is not null)
|
||||
return;
|
||||
|
||||
if (_markingsModel.TryGetMarking(_organ, _layer, _markingPrototype.ID) is not { } marking)
|
||||
return;
|
||||
|
||||
_colorSliders = new();
|
||||
|
||||
for (var i = 0; i < _markingPrototype.Sprites.Count; i++)
|
||||
{
|
||||
var container = new BoxContainer()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
ColorsContainer.AddChild(container);
|
||||
|
||||
var selector = new ColorSelectorSliders();
|
||||
selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv;
|
||||
|
||||
var label = _markingPrototype.Sprites[i] switch
|
||||
{
|
||||
SpriteSpecifier.Rsi rsi => Loc.GetString($"marking-{_markingPrototype.ID}-{rsi.RsiState}"),
|
||||
SpriteSpecifier.Texture texture => Loc.GetString($"marking-{_markingPrototype.ID}-{texture.TexturePath.Filename}"),
|
||||
_ => throw new InvalidOperationException("SpriteSpecifier not of known type"),
|
||||
};
|
||||
|
||||
container.AddChild(new Label { Text = label });
|
||||
container.AddChild(selector);
|
||||
|
||||
selector.Color = marking.MarkingColors[i];
|
||||
|
||||
_colorSliders.Add(selector);
|
||||
|
||||
var colorIndex = i;
|
||||
selector.OnColorChanged += _ =>
|
||||
{
|
||||
_markingsModel.TrySetMarkingColor(_organ, _layer, _markingPrototype.ID, colorIndex, selector.Color);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckMatchesSearch(string query)
|
||||
{
|
||||
return Loc.GetString($"marking-{_markingPrototype.ID}").Contains(query, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void SetHiddenState(bool state, string query)
|
||||
{
|
||||
Visible = CheckMatchesSearch(query) ? state : !state;
|
||||
}
|
||||
|
||||
private void OnPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
Pressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnUnpressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
Unpressed?.Invoke(args, this);
|
||||
}
|
||||
}
|
||||
3
Content.Client/Humanoid/LayerMarkingOrderer.xaml
Normal file
3
Content.Client/Humanoid/LayerMarkingOrderer.xaml
Normal file
@@ -0,0 +1,3 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical">
|
||||
<BoxContainer Name="Items" Margin="4" Orientation="Vertical" />
|
||||
</BoxContainer>
|
||||
192
Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs
Normal file
192
Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Interaction;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class LayerMarkingOrderer : BoxContainer
|
||||
{
|
||||
private readonly ProtoId<OrganCategoryPrototype> _organ;
|
||||
private readonly HumanoidVisualLayers _layer;
|
||||
private readonly MarkingsViewModel _markingsModel;
|
||||
private readonly DragDropHelper<LayerMarkingDragged> _dragDropHelper;
|
||||
private readonly List<LayerDragDropBeacon> _beacons = new();
|
||||
private LayerDragDropBeacon? _dragTarget;
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public LayerMarkingOrderer(MarkingsViewModel markingsModel, ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_markingsModel = markingsModel;
|
||||
_organ = organ;
|
||||
_layer = layer;
|
||||
_dragDropHelper = new(OnBeginDrag, OnContinueDrag, OnEndDrag);
|
||||
|
||||
UpdateItems();
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_markingsModel.MarkingsReset += UpdateItems;
|
||||
_markingsModel.MarkingsChanged += MarkingsChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_markingsModel.MarkingsReset -= UpdateItems;
|
||||
_markingsModel.MarkingsChanged -= MarkingsChanged;
|
||||
}
|
||||
|
||||
private void MarkingsChanged(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer)
|
||||
{
|
||||
if (_organ != organ || _layer != layer)
|
||||
return;
|
||||
|
||||
UpdateItems();
|
||||
}
|
||||
|
||||
private void UpdateItems()
|
||||
{
|
||||
Items.RemoveAllChildren();
|
||||
_beacons.Clear();
|
||||
|
||||
if (_markingsModel.SelectedMarkings(_organ, _layer) is not { } markings)
|
||||
return;
|
||||
|
||||
for (var idx = 0; idx < markings.Count; idx++)
|
||||
{
|
||||
var marking = markings[idx];
|
||||
|
||||
var container = new LayerMarkingItemContainer();
|
||||
container.Margin = new(4);
|
||||
|
||||
var item = new LayerMarkingItem(_markingsModel, _organ, _layer, _prototype.Index<MarkingPrototype>(marking.MarkingId), false);
|
||||
item.DefaultCursorShape = CursorShape.Hand;
|
||||
item.Pressed += (args, control) => OnItemPressed(args, control, container);
|
||||
item.Unpressed += OnItemUnpressed;
|
||||
|
||||
container.AddChild(item);
|
||||
|
||||
var before = new LayerDragDropBeacon(CandidatePosition.Before, idx);
|
||||
var after = new LayerDragDropBeacon(CandidatePosition.After, idx);
|
||||
_beacons.Add(before);
|
||||
_beacons.Add(after);
|
||||
|
||||
Items.AddChild(before);
|
||||
Items.AddChild(container);
|
||||
Items.AddChild(after);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemPressed(GUIBoundKeyEventArgs args, LayerMarkingItem control, LayerMarkingItemContainer container)
|
||||
{
|
||||
_dragDropHelper.MouseDown(new(control, container));
|
||||
}
|
||||
|
||||
private void OnItemUnpressed(GUIBoundKeyEventArgs args, LayerMarkingItem control)
|
||||
{
|
||||
_dragDropHelper.EndDrag();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
_dragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
|
||||
private bool OnBeginDrag()
|
||||
{
|
||||
var (item, container) = _dragDropHelper.Dragged;
|
||||
|
||||
container.Visible = false;
|
||||
item.Orphan();
|
||||
item.DefaultCursorShape = CursorShape.Move;
|
||||
UserInterfaceManager.PopupRoot.AddChild(item);
|
||||
LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnContinueDrag(float frameTime)
|
||||
{
|
||||
var (item, container) = _dragDropHelper.Dragged;
|
||||
|
||||
LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32));
|
||||
|
||||
var closestBeacon =
|
||||
_beacons.MinBy(beacon =>
|
||||
(UserInterfaceManager.MousePositionScaled.Position - beacon.GlobalPosition).LengthSquared());
|
||||
|
||||
if (closestBeacon != _dragTarget)
|
||||
{
|
||||
_dragTarget?.UnbecomeTarget();
|
||||
_dragTarget = closestBeacon;
|
||||
_dragTarget?.BecomeTarget();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnEndDrag()
|
||||
{
|
||||
var (item, container) = _dragDropHelper.Dragged;
|
||||
|
||||
container.Visible = true;
|
||||
item.Orphan();
|
||||
container.AddChild(item);
|
||||
_dragTarget?.UnbecomeTarget();
|
||||
|
||||
if (_dragTarget != null)
|
||||
{
|
||||
_markingsModel.ChangeMarkingOrder(_organ, _layer, item.MarkingId, _dragTarget.CandidatePosition, _dragTarget.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct LayerMarkingDragged(LayerMarkingItem Item, LayerMarkingItemContainer Container);
|
||||
|
||||
internal sealed class LayerMarkingItemContainer : PanelContainer
|
||||
{
|
||||
public LayerMarkingItemContainer()
|
||||
{
|
||||
SetHeight = 64;
|
||||
HorizontalExpand = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LayerDragDropBeacon(CandidatePosition position, int index) : PanelContainer
|
||||
{
|
||||
public readonly CandidatePosition CandidatePosition = position;
|
||||
public readonly int Index = index;
|
||||
|
||||
public void BecomeTarget()
|
||||
{
|
||||
SetHeight = 64;
|
||||
HorizontalExpand = true;
|
||||
SetOnlyStyleClass(StyleClass.PanelDropTarget);
|
||||
}
|
||||
|
||||
public void UnbecomeTarget()
|
||||
{
|
||||
SetHeight = float.NaN;
|
||||
RemoveStyleClass(StyleClass.PanelDropTarget);
|
||||
}
|
||||
}
|
||||
12
Content.Client/Humanoid/LayerMarkingPicker.xaml
Normal file
12
Content.Client/Humanoid/LayerMarkingPicker.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'markings-search'}" HorizontalExpand="True" />
|
||||
<ScrollContainer Name="SelectionItems" HorizontalExpand="True" VerticalExpand="True">
|
||||
<GridContainer Name="Items" Columns="2" Margin="4" />
|
||||
</ScrollContainer>
|
||||
<ScrollContainer Name="OrderingItems" HorizontalExpand="True" VerticalExpand="True" Visible="False">
|
||||
</ScrollContainer>
|
||||
<BoxContainer HorizontalExpand="True" Margin="4">
|
||||
<Label Name="MarkingsStatus" HorizontalExpand="True" />
|
||||
<Button Name="ReorderButton" ToggleMode="True" Text="{Loc 'markings-reorder'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
113
Content.Client/Humanoid/LayerMarkingPicker.xaml.cs
Normal file
113
Content.Client/Humanoid/LayerMarkingPicker.xaml.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.ControlExtensions;
|
||||
using Content.Client.Guidebook.Controls;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class LayerMarkingPicker : BoxContainer
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, MarkingPrototype> _allMarkings;
|
||||
private readonly ProtoId<OrganCategoryPrototype> _organ;
|
||||
private readonly HumanoidVisualLayers _layer;
|
||||
private readonly MarkingsViewModel _markingsModel;
|
||||
private List<ISearchableControl> _searchable = new();
|
||||
private const int _columnWidth = 500;
|
||||
|
||||
public LayerMarkingPicker(MarkingsViewModel markingsModel, ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, IReadOnlyDictionary<string, MarkingPrototype> allMarkings)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_markingsModel = markingsModel;
|
||||
_allMarkings = allMarkings;
|
||||
_organ = organ;
|
||||
_layer = layer;
|
||||
|
||||
OrderingItems.AddChild(new LayerMarkingOrderer(markingsModel, organ, layer));
|
||||
|
||||
UpdateMarkings();
|
||||
|
||||
SearchBar.OnTextChanged += _ =>
|
||||
{
|
||||
foreach (var element in _searchable)
|
||||
{
|
||||
element.SetHiddenState(true, SearchBar.Text.Trim());
|
||||
}
|
||||
};
|
||||
|
||||
UpdateCount();
|
||||
|
||||
ReorderButton.OnPressed += ReorderButtonPressed;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_markingsModel.MarkingsReset += UpdateCount;
|
||||
_markingsModel.MarkingsChanged += MarkingsChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_markingsModel.MarkingsReset -= UpdateCount;
|
||||
_markingsModel.MarkingsChanged -= MarkingsChanged;
|
||||
}
|
||||
|
||||
private void MarkingsChanged(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer)
|
||||
{
|
||||
if (_organ != organ || _layer != layer)
|
||||
return;
|
||||
|
||||
UpdateCount();
|
||||
}
|
||||
|
||||
private void UpdateMarkings()
|
||||
{
|
||||
foreach (var marking in _allMarkings.Values.OrderBy(marking => Loc.GetString($"marking-{marking.ID}")))
|
||||
{
|
||||
var item = new LayerMarkingItem(_markingsModel, _organ, _layer, marking, true);
|
||||
Items.AddChild(item);
|
||||
}
|
||||
_searchable = Items.GetSearchableControls();
|
||||
}
|
||||
|
||||
private void UpdateCount()
|
||||
{
|
||||
_markingsModel.GetMarkingCounts(_organ, _layer, out var isRequired, out var count, out var selected);
|
||||
MarkingsStatus.Text = Loc.GetString("markings-limits", ("required", isRequired), ("count", count), ("selectable", count - selected));
|
||||
}
|
||||
|
||||
private void ReorderButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (ReorderButton.Pressed)
|
||||
{
|
||||
SelectionItems.Visible = false;
|
||||
SearchBar.Visible = false;
|
||||
OrderingItems.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectionItems.Visible = true;
|
||||
SearchBar.Visible = true;
|
||||
OrderingItems.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
|
||||
Items.Columns = (int)(Width / _columnWidth);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,3 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<!-- Primary container -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Marking lists -->
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="5" HorizontalExpand="True">
|
||||
<!-- Unused markings -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'markings-unused'}" HorizontalAlignment="Stretch" HorizontalExpand="True" />
|
||||
<Label Name="CMarkingPoints" Text="uwu" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
|
||||
<OptionButton Name="CMarkingCategoryButton" StyleClasses="OpenLeft" />
|
||||
<LineEdit Name="CMarkingSearch" PlaceHolder="{Loc 'markings-search'}" />
|
||||
|
||||
<ItemList Name="CMarkingsUnused" VerticalExpand="True" MinSize="300 250" />
|
||||
<Button Name="CMarkingAdd" Text="{Loc 'markings-add'}" StyleClasses="OpenRight" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Used markings -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'markings-used'}" />
|
||||
|
||||
<ItemList Name="CMarkingsUsed" VerticalExpand="True" MinSize="300 250" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="CMarkingRankUp" Text="{Loc 'markings-rank-up'}" StyleClasses="OpenBoth" HorizontalExpand="True" />
|
||||
<Button Name="CMarkingRankDown" Text="{Loc 'markings-rank-down'}" StyleClasses="OpenBoth" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<Button Name="CMarkingRemove" Text="{Loc 'markings-remove'}" StyleClasses="OpenRight" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Colors -->
|
||||
<BoxContainer Name="CMarkingColors" Orientation="Vertical" Visible="False" />
|
||||
</BoxContainer>
|
||||
<TabContainer Name="OrganTabs" />
|
||||
</Control>
|
||||
|
||||
@@ -1,553 +1,68 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MarkingPicker : Control
|
||||
{
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public Action<MarkingSet>? OnMarkingAdded;
|
||||
public Action<MarkingSet>? OnMarkingRemoved;
|
||||
public Action<MarkingSet>? OnMarkingColorChange;
|
||||
public Action<MarkingSet>? OnMarkingRankChange;
|
||||
|
||||
private List<Color> _currentMarkingColors = new();
|
||||
|
||||
private ItemList.Item? _selectedMarking;
|
||||
private ItemList.Item? _selectedUnusedMarking;
|
||||
private MarkingCategories _selectedMarkingCategory = MarkingCategories.Chest;
|
||||
|
||||
private MarkingSet _currentMarkings = new();
|
||||
|
||||
private List<MarkingCategories> _markingCategories = Enum.GetValues<MarkingCategories>().ToList();
|
||||
|
||||
private string _currentSpecies = SharedHumanoidAppearanceSystem.DefaultSpecies;
|
||||
private Sex _currentSex = Sex.Unsexed;
|
||||
public Color CurrentSkinColor = Color.White;
|
||||
public Color CurrentEyeColor = Color.Black;
|
||||
public Marking? HairMarking;
|
||||
public Marking? FacialHairMarking;
|
||||
|
||||
private readonly HashSet<MarkingCategories> _ignoreCategories = new();
|
||||
|
||||
public string IgnoreCategories
|
||||
{
|
||||
get => string.Join(',', _ignoreCategories);
|
||||
set
|
||||
{
|
||||
_ignoreCategories.Clear();
|
||||
var split = value.Split(',');
|
||||
foreach (var category in split)
|
||||
{
|
||||
if (!Enum.TryParse(category, out MarkingCategories categoryParse))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ignoreCategories.Add(categoryParse);
|
||||
}
|
||||
|
||||
SetupCategoryButtons();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Forced { get; set; }
|
||||
|
||||
private bool _ignoreSpecies;
|
||||
|
||||
public bool IgnoreSpecies
|
||||
{
|
||||
get => _ignoreSpecies;
|
||||
set
|
||||
{
|
||||
_ignoreSpecies = value;
|
||||
Populate(CMarkingSearch.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetData(List<Marking> newMarkings, string species, Sex sex, Color skinColor, Color eyeColor)
|
||||
{
|
||||
var pointsProto = _prototypeManager
|
||||
.Index<SpeciesPrototype>(species).MarkingPoints;
|
||||
_currentMarkings = new(newMarkings, pointsProto, _markingManager);
|
||||
|
||||
if (!IgnoreSpecies)
|
||||
{
|
||||
_currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt
|
||||
}
|
||||
|
||||
_currentSpecies = species;
|
||||
_currentSex = sex;
|
||||
CurrentSkinColor = skinColor;
|
||||
CurrentEyeColor = eyeColor;
|
||||
|
||||
Populate(CMarkingSearch.Text);
|
||||
PopulateUsed();
|
||||
}
|
||||
|
||||
public void SetData(MarkingSet set, string species, Sex sex, Color skinColor, Color eyeColor)
|
||||
{
|
||||
_currentMarkings = set;
|
||||
|
||||
if (!IgnoreSpecies)
|
||||
{
|
||||
_currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt
|
||||
}
|
||||
|
||||
_currentSpecies = species;
|
||||
_currentSex = sex;
|
||||
CurrentSkinColor = skinColor;
|
||||
CurrentEyeColor = eyeColor;
|
||||
|
||||
Populate(CMarkingSearch.Text);
|
||||
PopulateUsed();
|
||||
}
|
||||
|
||||
public void SetSkinColor(Color color) => CurrentSkinColor = color;
|
||||
public void SetEyeColor(Color color) => CurrentEyeColor = color;
|
||||
private MarkingsViewModel? _markingsModel;
|
||||
|
||||
public MarkingPicker()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
|
||||
CMarkingCategoryButton.OnItemSelected += OnCategoryChange;
|
||||
CMarkingsUnused.OnItemSelected += item =>
|
||||
_selectedUnusedMarking = CMarkingsUnused[item.ItemIndex];
|
||||
|
||||
CMarkingAdd.OnPressed += _ =>
|
||||
MarkingAdd();
|
||||
|
||||
CMarkingsUsed.OnItemSelected += OnUsedMarkingSelected;
|
||||
|
||||
CMarkingRemove.OnPressed += _ =>
|
||||
MarkingRemove();
|
||||
|
||||
CMarkingRankUp.OnPressed += _ => SwapMarkingUp();
|
||||
CMarkingRankDown.OnPressed += _ => SwapMarkingDown();
|
||||
|
||||
CMarkingSearch.OnTextChanged += args => Populate(args.Text);
|
||||
UpdateMarkings();
|
||||
}
|
||||
|
||||
private void SetupCategoryButtons()
|
||||
public void SetModel(MarkingsViewModel model)
|
||||
{
|
||||
CMarkingCategoryButton.Clear();
|
||||
_markingsModel = model;
|
||||
|
||||
var validCategories = new List<MarkingCategories>();
|
||||
for (var i = 0; i < _markingCategories.Count; i++)
|
||||
_markingsModel.OrganDataChanged += UpdateMarkings;
|
||||
_markingsModel.EnforcementsChanged += UpdateMarkings;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_markingsModel?.OrganDataChanged += UpdateMarkings;
|
||||
_markingsModel?.EnforcementsChanged += UpdateMarkings;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_markingsModel?.OrganDataChanged -= UpdateMarkings;
|
||||
_markingsModel?.EnforcementsChanged -= UpdateMarkings;
|
||||
}
|
||||
|
||||
private void UpdateMarkings()
|
||||
{
|
||||
if (_markingsModel is null)
|
||||
return;
|
||||
|
||||
OrganTabs.RemoveAllChildren();
|
||||
|
||||
var i = 0;
|
||||
foreach (var (organ, organData) in _markingsModel.OrganData)
|
||||
{
|
||||
var category = _markingCategories[i];
|
||||
var markings = GetMarkings(category);
|
||||
if (_ignoreCategories.Contains(category) ||
|
||||
markings.Count == 0)
|
||||
{
|
||||
var control = new OrganMarkingPicker(_markingsModel, organ, organData.Layers, organData.Group);
|
||||
if (control.Empty)
|
||||
continue;
|
||||
}
|
||||
|
||||
validCategories.Add(category);
|
||||
CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{category.ToString()}"), i);
|
||||
OrganTabs.AddChild(control);
|
||||
OrganTabs.SetTabTitle(i, Loc.GetString($"markings-organ-{organ.Id}"));
|
||||
i++;
|
||||
}
|
||||
|
||||
if (validCategories.Contains(_selectedMarkingCategory))
|
||||
{
|
||||
CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory));
|
||||
}
|
||||
else if (validCategories.Count > 0)
|
||||
{
|
||||
_selectedMarkingCategory = validCategories[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedMarkingCategory = MarkingCategories.Chest;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}");
|
||||
|
||||
private List<string> GetMarkingStateNames(MarkingPrototype marking)
|
||||
{
|
||||
List<string> result = new();
|
||||
foreach (var markingState in marking.Sprites)
|
||||
{
|
||||
switch (markingState)
|
||||
{
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
result.Add(Loc.GetString($"marking-{marking.ID}-{rsi.RsiState}"));
|
||||
break;
|
||||
case SpriteSpecifier.Texture texture:
|
||||
result.Add(Loc.GetString($"marking-{marking.ID}-{texture.TexturePath.Filename}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, MarkingPrototype> GetMarkings(MarkingCategories category)
|
||||
{
|
||||
return IgnoreSpecies
|
||||
? _markingManager.MarkingsByCategoryAndSex(category, _currentSex)
|
||||
: _markingManager.MarkingsByCategoryAndSpeciesAndSex(category, _currentSpecies, _currentSex);
|
||||
}
|
||||
|
||||
public void Populate(string filter)
|
||||
{
|
||||
SetupCategoryButtons();
|
||||
|
||||
CMarkingsUnused.Clear();
|
||||
_selectedUnusedMarking = null;
|
||||
|
||||
var sortedMarkings = GetMarkings(_selectedMarkingCategory).Values.Where(m =>
|
||||
m.ID.ToLower().Contains(filter.ToLower()) ||
|
||||
GetMarkingName(m).ToLower().Contains(filter.ToLower())
|
||||
).OrderBy(p => Loc.GetString(GetMarkingName(p)));
|
||||
|
||||
foreach (var marking in sortedMarkings)
|
||||
{
|
||||
if (_currentMarkings.TryGetMarking(_selectedMarkingCategory, marking.ID, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", _sprite.Frame0(marking.Sprites[0]));
|
||||
item.Metadata = marking;
|
||||
}
|
||||
|
||||
CMarkingPoints.Visible = _currentMarkings.PointsLeft(_selectedMarkingCategory) != -1;
|
||||
}
|
||||
|
||||
// Populate the used marking list. Returns a list of markings that weren't
|
||||
// valid to add to the marking list.
|
||||
public void PopulateUsed()
|
||||
{
|
||||
CMarkingsUsed.Clear();
|
||||
CMarkingColors.Visible = false;
|
||||
_selectedMarking = null;
|
||||
|
||||
if (!IgnoreSpecies)
|
||||
{
|
||||
_currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager);
|
||||
}
|
||||
|
||||
// walk backwards through the list for visual purposes
|
||||
foreach (var marking in _currentMarkings.GetReverseEnumerator(_selectedMarkingCategory))
|
||||
{
|
||||
if (!_markingManager.TryGetMarking(marking, out var newMarking))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = Loc.GetString(marking.Forced ? "marking-used-forced" : "marking-used", ("marking-name", $"{GetMarkingName(newMarking)}"),
|
||||
("marking-category", Loc.GetString($"markings-category-{newMarking.MarkingCategory}")));
|
||||
|
||||
var _item = new ItemList.Item(CMarkingsUsed)
|
||||
{
|
||||
Text = text,
|
||||
Icon = _sprite.Frame0(newMarking.Sprites[0]),
|
||||
Selectable = true,
|
||||
Metadata = newMarking,
|
||||
IconModulate = marking.MarkingColors[0]
|
||||
};
|
||||
|
||||
CMarkingsUsed.Add(_item);
|
||||
}
|
||||
|
||||
// since all the points have been processed, update the points visually
|
||||
UpdatePoints();
|
||||
}
|
||||
|
||||
private void SwapMarkingUp()
|
||||
{
|
||||
if (_selectedMarking == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var i = CMarkingsUsed.IndexOf(_selectedMarking);
|
||||
if (ShiftMarkingRank(i, -1))
|
||||
{
|
||||
OnMarkingRankChange?.Invoke(_currentMarkings);
|
||||
}
|
||||
}
|
||||
|
||||
private void SwapMarkingDown()
|
||||
{
|
||||
if (_selectedMarking == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var i = CMarkingsUsed.IndexOf(_selectedMarking);
|
||||
if (ShiftMarkingRank(i, 1))
|
||||
{
|
||||
OnMarkingRankChange?.Invoke(_currentMarkings);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShiftMarkingRank(int src, int places)
|
||||
{
|
||||
if (src + places >= CMarkingsUsed.Count || src + places < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var visualDest = src + places; // what it would visually look like
|
||||
var visualTemp = CMarkingsUsed[visualDest];
|
||||
CMarkingsUsed[visualDest] = CMarkingsUsed[src];
|
||||
CMarkingsUsed[src] = visualTemp;
|
||||
|
||||
switch (places)
|
||||
{
|
||||
// i.e., we're going down in rank
|
||||
case < 0:
|
||||
_currentMarkings.ShiftRankDownFromEnd(_selectedMarkingCategory, src);
|
||||
break;
|
||||
// i.e., we're going up in rank
|
||||
case > 0:
|
||||
_currentMarkings.ShiftRankUpFromEnd(_selectedMarkingCategory, src);
|
||||
break;
|
||||
// do nothing?
|
||||
// ReSharper disable once RedundantEmptySwitchSection
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// repopulate in case markings are restricted,
|
||||
// and also filter out any markings that are now invalid
|
||||
// attempt to preserve any existing markings as well:
|
||||
// it would be frustrating to otherwise have all markings
|
||||
// cleared, imo
|
||||
public void SetSpecies(string species)
|
||||
{
|
||||
_currentSpecies = species;
|
||||
var markingList = _currentMarkings.GetForwardEnumerator().ToList();
|
||||
|
||||
var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(species);
|
||||
|
||||
_currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
|
||||
_currentMarkings.EnsureSpecies(species, null, _markingManager);
|
||||
_currentMarkings.EnsureSexes(_currentSex, _markingManager);
|
||||
|
||||
Populate(CMarkingSearch.Text);
|
||||
PopulateUsed();
|
||||
}
|
||||
|
||||
public void SetSex(Sex sex)
|
||||
{
|
||||
_currentSex = sex;
|
||||
var markingList = _currentMarkings.GetForwardEnumerator().ToList();
|
||||
|
||||
var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(_currentSpecies);
|
||||
|
||||
_currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
|
||||
_currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager);
|
||||
_currentMarkings.EnsureSexes(_currentSex, _markingManager);
|
||||
|
||||
Populate(CMarkingSearch.Text);
|
||||
PopulateUsed();
|
||||
}
|
||||
|
||||
private void UpdatePoints()
|
||||
{
|
||||
var count = _currentMarkings.PointsLeft(_selectedMarkingCategory);
|
||||
if (count > -1)
|
||||
{
|
||||
CMarkingPoints.Text = Loc.GetString("marking-points-remaining", ("points", count));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCategoryChange(OptionButton.ItemSelectedEventArgs category)
|
||||
{
|
||||
CMarkingCategoryButton.SelectId(category.Id);
|
||||
_selectedMarkingCategory = _markingCategories[category.Id];
|
||||
Populate(CMarkingSearch.Text);
|
||||
PopulateUsed();
|
||||
UpdatePoints();
|
||||
}
|
||||
|
||||
// TODO: This should be using ColorSelectorSliders once that's merged, so
|
||||
private void OnUsedMarkingSelected(ItemList.ItemListSelectedEventArgs item)
|
||||
{
|
||||
_selectedMarking = CMarkingsUsed[item.ItemIndex];
|
||||
var prototype = (MarkingPrototype) _selectedMarking.Metadata!;
|
||||
|
||||
if (prototype.ForcedColoring)
|
||||
{
|
||||
CMarkingColors.Visible = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var stateNames = GetMarkingStateNames(prototype);
|
||||
_currentMarkingColors.Clear();
|
||||
CMarkingColors.RemoveAllChildren();
|
||||
List<ColorSelectorSliders> colorSliders = new();
|
||||
for (int i = 0; i < prototype.Sprites.Count; i++)
|
||||
{
|
||||
var colorContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
};
|
||||
|
||||
CMarkingColors.AddChild(colorContainer);
|
||||
|
||||
ColorSelectorSliders colorSelector = new ColorSelectorSliders();
|
||||
colorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
|
||||
colorSliders.Add(colorSelector);
|
||||
|
||||
colorContainer.AddChild(new Label { Text = $"{stateNames[i]} color:" });
|
||||
colorContainer.AddChild(colorSelector);
|
||||
|
||||
var listing = _currentMarkings.Markings[_selectedMarkingCategory];
|
||||
|
||||
var color = listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i];
|
||||
var currentColor = new Color(
|
||||
color.RByte,
|
||||
color.GByte,
|
||||
color.BByte
|
||||
);
|
||||
colorSelector.Color = currentColor;
|
||||
_currentMarkingColors.Add(currentColor);
|
||||
var colorIndex = _currentMarkingColors.Count - 1;
|
||||
|
||||
Action<Color> colorChanged = _ =>
|
||||
{
|
||||
_currentMarkingColors[colorIndex] = colorSelector.Color;
|
||||
|
||||
ColorChanged(colorIndex);
|
||||
};
|
||||
colorSelector.OnColorChanged += colorChanged;
|
||||
}
|
||||
|
||||
CMarkingColors.Visible = true;
|
||||
}
|
||||
|
||||
private void ColorChanged(int colorIndex)
|
||||
{
|
||||
if (_selectedMarking is null) return;
|
||||
var markingPrototype = (MarkingPrototype) _selectedMarking.Metadata!;
|
||||
int markingIndex = _currentMarkings.FindIndexOf(_selectedMarkingCategory, markingPrototype.ID);
|
||||
|
||||
if (markingIndex < 0) return;
|
||||
|
||||
_selectedMarking.IconModulate = _currentMarkingColors[colorIndex];
|
||||
|
||||
var marking = new Marking(_currentMarkings.Markings[_selectedMarkingCategory][markingIndex]);
|
||||
marking.SetColor(colorIndex, _currentMarkingColors[colorIndex]);
|
||||
_currentMarkings.Replace(_selectedMarkingCategory, markingIndex, marking);
|
||||
|
||||
OnMarkingColorChange?.Invoke(_currentMarkings);
|
||||
}
|
||||
|
||||
private void MarkingAdd()
|
||||
{
|
||||
if (_selectedUnusedMarking is null) return;
|
||||
|
||||
if (_currentMarkings.PointsLeft(_selectedMarkingCategory) == 0 && !Forced)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var marking = (MarkingPrototype) _selectedUnusedMarking.Metadata!;
|
||||
var markingObject = marking.AsMarking();
|
||||
|
||||
// We need add hair markings in cloned set manually because _currentMarkings doesn't have it
|
||||
var markingSet = new MarkingSet(_currentMarkings);
|
||||
if (HairMarking != null)
|
||||
{
|
||||
markingSet.AddBack(MarkingCategories.Hair, HairMarking);
|
||||
}
|
||||
if (FacialHairMarking != null)
|
||||
{
|
||||
markingSet.AddBack(MarkingCategories.FacialHair, FacialHairMarking);
|
||||
}
|
||||
|
||||
if (!_markingManager.MustMatchSkin(_currentSpecies, marking.BodyPart, out var _, _prototypeManager))
|
||||
{
|
||||
// Do default coloring
|
||||
var colors = MarkingColoring.GetMarkingLayerColors(
|
||||
marking,
|
||||
CurrentSkinColor,
|
||||
CurrentEyeColor,
|
||||
markingSet
|
||||
);
|
||||
for (var i = 0; i < colors.Count; i++)
|
||||
{
|
||||
markingObject.SetColor(i, colors[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Color everything in skin color
|
||||
for (var i = 0; i < marking.Sprites.Count; i++)
|
||||
{
|
||||
markingObject.SetColor(i, CurrentSkinColor);
|
||||
}
|
||||
}
|
||||
|
||||
markingObject.Forced = Forced;
|
||||
|
||||
_currentMarkings.AddBack(_selectedMarkingCategory, markingObject);
|
||||
|
||||
UpdatePoints();
|
||||
|
||||
CMarkingsUnused.Remove(_selectedUnusedMarking);
|
||||
var item = new ItemList.Item(CMarkingsUsed)
|
||||
{
|
||||
Text = Loc.GetString("marking-used", ("marking-name", $"{GetMarkingName(marking)}"), ("marking-category", Loc.GetString($"markings-category-{marking.MarkingCategory}"))),
|
||||
Icon = _sprite.Frame0(marking.Sprites[0]),
|
||||
Selectable = true,
|
||||
Metadata = marking,
|
||||
};
|
||||
CMarkingsUsed.Insert(0, item);
|
||||
|
||||
_selectedUnusedMarking = null;
|
||||
OnMarkingAdded?.Invoke(_currentMarkings);
|
||||
}
|
||||
|
||||
private void MarkingRemove()
|
||||
{
|
||||
if (_selectedMarking is null) return;
|
||||
|
||||
var marking = (MarkingPrototype) _selectedMarking.Metadata!;
|
||||
|
||||
_currentMarkings.Remove(_selectedMarkingCategory, marking.ID);
|
||||
|
||||
UpdatePoints();
|
||||
|
||||
CMarkingsUsed.Remove(_selectedMarking);
|
||||
|
||||
if (marking.MarkingCategory == _selectedMarkingCategory)
|
||||
{
|
||||
var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", _sprite.Frame0(marking.Sprites[0]));
|
||||
item.Metadata = marking;
|
||||
}
|
||||
_selectedMarking = null;
|
||||
CMarkingColors.Visible = false;
|
||||
OnMarkingRemoved?.Invoke(_currentMarkings);
|
||||
if (i > 0)
|
||||
OrganTabs.CurrentTab = 0;
|
||||
OrganTabs.TabsVisible = i > 1;
|
||||
}
|
||||
}
|
||||
|
||||
392
Content.Client/Humanoid/MarkingsViewModel.cs
Normal file
392
Content.Client/Humanoid/MarkingsViewModel.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
public sealed class MarkingsViewModel
|
||||
{
|
||||
[Dependency] private readonly MarkingManager _marking = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private bool _enforceLimits = true;
|
||||
|
||||
public bool EnforceLimits
|
||||
{
|
||||
get => _enforceLimits;
|
||||
set
|
||||
{
|
||||
if (_enforceLimits == value)
|
||||
return;
|
||||
|
||||
_enforceLimits = value;
|
||||
EnforcementsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enforceGroupAndSexRestrictions = true;
|
||||
|
||||
public bool EnforceGroupAndSexRestrictions
|
||||
{
|
||||
get => _enforceGroupAndSexRestrictions;
|
||||
set
|
||||
{
|
||||
if (_enforceGroupAndSexRestrictions == value)
|
||||
return;
|
||||
|
||||
_enforceGroupAndSexRestrictions = value;
|
||||
EnforcementsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private bool AnyEnforcementsLifted => !_enforceLimits || !_enforceGroupAndSexRestrictions;
|
||||
|
||||
public event Action? EnforcementsChanged;
|
||||
|
||||
private Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> _organProfileData = new();
|
||||
|
||||
public Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> OrganProfileData
|
||||
{
|
||||
get => _organProfileData;
|
||||
set
|
||||
{
|
||||
_organProfileData = value.ShallowClone();
|
||||
OrganProfileDataChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOrganSexes(Sex sex)
|
||||
{
|
||||
foreach (var (organ, data) in _organProfileData)
|
||||
{
|
||||
_organProfileData[organ] = data with { Sex = sex };
|
||||
}
|
||||
OrganProfileDataChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetOrganSkinColor(Color skinColor)
|
||||
{
|
||||
foreach (var (organ, data) in _organProfileData)
|
||||
{
|
||||
_organProfileData[organ] = data with { SkinColor = skinColor };
|
||||
}
|
||||
OrganProfileDataChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetOrganEyeColor(Color eyeColor)
|
||||
{
|
||||
foreach (var (organ, data) in _organProfileData)
|
||||
{
|
||||
_organProfileData[organ] = data with { EyeColor = eyeColor };
|
||||
}
|
||||
OrganProfileDataChanged?.Invoke();
|
||||
}
|
||||
|
||||
public event Action? OrganProfileDataChanged;
|
||||
|
||||
private Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> _markings = new();
|
||||
|
||||
public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings
|
||||
{
|
||||
get => _markings;
|
||||
set
|
||||
{
|
||||
_markings = value.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.ToDictionary(
|
||||
it => it.Key,
|
||||
it => it.Value.Select(marking => new Marking(marking)).ToList()));
|
||||
|
||||
MarkingsReset?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? MarkingsReset;
|
||||
|
||||
public event Action<ProtoId<OrganCategoryPrototype>, HumanoidVisualLayers>? MarkingsChanged;
|
||||
|
||||
private Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> _organData = new();
|
||||
|
||||
public Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData>
|
||||
OrganData
|
||||
{
|
||||
get => _organData;
|
||||
set
|
||||
{
|
||||
if (_organData == value)
|
||||
return;
|
||||
|
||||
_organData = value;
|
||||
_previousColors.Clear();
|
||||
OrganDataChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? OrganDataChanged;
|
||||
|
||||
private Dictionary<ProtoId<MarkingPrototype>, List<Color>> _previousColors = new();
|
||||
|
||||
public MarkingsViewModel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public bool IsMarkingSelected(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId)
|
||||
{
|
||||
return TryGetMarking(organ, layer, markingId) is not null;
|
||||
}
|
||||
|
||||
public bool IsMarkingColorCustomizable(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId)
|
||||
{
|
||||
if (!_prototype.TryIndex(markingId, out var markingProto))
|
||||
return false;
|
||||
|
||||
if (markingProto.ForcedColoring)
|
||||
return false;
|
||||
|
||||
if (!_organData.TryGetValue(organ, out var organData))
|
||||
return false;
|
||||
|
||||
if (!_prototype.TryIndex(organData.Group, out var groupProto))
|
||||
return false;
|
||||
|
||||
if (!groupProto.Appearances.TryGetValue(layer, out var appearance))
|
||||
return true;
|
||||
|
||||
return !appearance.MatchSkin;
|
||||
}
|
||||
|
||||
public Marking? TryGetMarking(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId)
|
||||
{
|
||||
if (!_markings.TryGetValue(organ, out var markingSet))
|
||||
return null;
|
||||
|
||||
if (!markingSet.TryGetValue(layer, out var markings))
|
||||
return null;
|
||||
|
||||
return markings.FirstOrDefault(it => it.MarkingId == markingId);
|
||||
}
|
||||
|
||||
public bool TrySelectMarking(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId)
|
||||
{
|
||||
if (!_prototype.TryIndex(markingId, out var markingProto))
|
||||
return false;
|
||||
|
||||
if (!_organData.TryGetValue(organ, out var organData) || !_organProfileData.TryGetValue(organ, out var profileData))
|
||||
return false;
|
||||
|
||||
if (!organData.Layers.Contains(layer))
|
||||
return false;
|
||||
|
||||
if (!_prototype.TryIndex(organData.Group, out var groupPrototype))
|
||||
return false;
|
||||
|
||||
if (EnforceGroupAndSexRestrictions && !_marking.CanBeApplied(organData.Group, profileData.Sex, markingProto))
|
||||
return false;
|
||||
|
||||
_markings[organ] = _markings.GetValueOrDefault(organ) ?? [];
|
||||
var organMarkings = _markings[organ];
|
||||
organMarkings[layer] = organMarkings.GetValueOrDefault(layer) ?? [];
|
||||
var layerMarkings = organMarkings[layer];
|
||||
|
||||
var colors = _previousColors.GetValueOrDefault(markingId) ??
|
||||
MarkingColoring.GetMarkingLayerColors(markingProto, profileData.SkinColor, profileData.EyeColor, layerMarkings);
|
||||
var newMarking = new Marking(markingId, colors);
|
||||
newMarking.Forced = AnyEnforcementsLifted;
|
||||
|
||||
var limits = groupPrototype.Limits.GetValueOrDefault(layer);
|
||||
if (limits is null || !EnforceLimits)
|
||||
{
|
||||
layerMarkings.Add(newMarking);
|
||||
MarkingsChanged?.Invoke(organ, layer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (limits.Limit == 1 && layerMarkings.Count == 1)
|
||||
{
|
||||
layerMarkings.Clear();
|
||||
layerMarkings.Add(newMarking);
|
||||
MarkingsChanged?.Invoke(organ, layer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (layerMarkings.Count < limits.Limit)
|
||||
{
|
||||
layerMarkings.Add(newMarking);
|
||||
MarkingsChanged?.Invoke(organ, layer);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<Marking>? SelectedMarkings(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer)
|
||||
{
|
||||
if (!_markings.TryGetValue(organ, out var organMarkings))
|
||||
return null;
|
||||
|
||||
if (!organMarkings.TryGetValue(layer, out var layerMarkings))
|
||||
return null;
|
||||
|
||||
return layerMarkings;
|
||||
}
|
||||
|
||||
public bool TryDeselectMarking(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId)
|
||||
{
|
||||
if (!_organData.TryGetValue(organ, out var organData))
|
||||
return false;
|
||||
|
||||
if (!organData.Layers.Contains(layer))
|
||||
return false;
|
||||
|
||||
if (!_prototype.TryIndex(organData.Group, out var groupPrototype))
|
||||
return false;
|
||||
|
||||
var limits = groupPrototype.Limits.GetValueOrDefault(layer);
|
||||
|
||||
_markings[organ] = _markings.GetValueOrDefault(organ) ?? [];
|
||||
var organMarkings = _markings[organ];
|
||||
organMarkings[layer] = organMarkings.GetValueOrDefault(layer) ?? [];
|
||||
var layerMarkings = organMarkings[layer];
|
||||
|
||||
var count = layerMarkings.Count(marking => marking.MarkingId == markingId);
|
||||
if (count == 0)
|
||||
return false;
|
||||
|
||||
if (EnforceLimits && limits is not null && limits.Required && (layerMarkings.Count - count) <= 0)
|
||||
return false;
|
||||
|
||||
if (layerMarkings.Find(marking => marking.MarkingId == markingId) is { } removingMarking)
|
||||
{
|
||||
_previousColors[removingMarking.MarkingId] = removingMarking.MarkingColors.ToList();
|
||||
}
|
||||
layerMarkings.RemoveAll(marking => marking.MarkingId == markingId);
|
||||
MarkingsChanged?.Invoke(organ, layer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void TrySetMarkingColor(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId,
|
||||
int colorIndex,
|
||||
Color color)
|
||||
{
|
||||
if (!_markings.TryGetValue(organ, out var markingSet))
|
||||
return;
|
||||
|
||||
if (!markingSet.TryGetValue(layer, out var markings))
|
||||
return;
|
||||
|
||||
if (markings.FirstOrDefault(it => it.MarkingId == markingId) is not { } marking)
|
||||
return;
|
||||
|
||||
marking.SetColor(colorIndex, color);
|
||||
MarkingsChanged?.Invoke(organ, layer);
|
||||
}
|
||||
|
||||
public void ValidateMarkings()
|
||||
{
|
||||
foreach (var (organ, organData) in _organData)
|
||||
{
|
||||
if (!_organProfileData.TryGetValue(organ, out var organProfileData))
|
||||
{
|
||||
_markings.Remove(organ);
|
||||
continue;
|
||||
}
|
||||
|
||||
var actualMarkings = _markings.GetValueOrDefault(organ)?.ShallowClone() ?? [];
|
||||
|
||||
_marking.EnsureValidColors(actualMarkings);
|
||||
_marking.EnsureValidGroupAndSex(actualMarkings, organData.Group, organProfileData.Sex);
|
||||
_marking.EnsureValidLayers(actualMarkings, organData.Layers);
|
||||
_marking.EnsureValidLimits(actualMarkings, organData.Group, organData.Layers, organProfileData.SkinColor, organProfileData.EyeColor);
|
||||
|
||||
_markings[organ] = actualMarkings;
|
||||
}
|
||||
|
||||
MarkingsReset?.Invoke();
|
||||
}
|
||||
|
||||
public void GetMarkingCounts(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, out bool isRequired, out int count, out int selected)
|
||||
{
|
||||
isRequired = false;
|
||||
count = 0;
|
||||
selected = 0;
|
||||
|
||||
if (!_organData.TryGetValue(organ, out var organData))
|
||||
return;
|
||||
|
||||
if (!organData.Layers.Contains(layer))
|
||||
return;
|
||||
|
||||
if (!_prototype.TryIndex(organData.Group, out var groupPrototype))
|
||||
return;
|
||||
|
||||
if (!groupPrototype.Limits.TryGetValue(layer, out var limits))
|
||||
return;
|
||||
|
||||
isRequired = limits.Required;
|
||||
count = limits.Limit;
|
||||
|
||||
if (!_markings.TryGetValue(organ, out var organMarkings))
|
||||
return;
|
||||
|
||||
if (!organMarkings.TryGetValue(layer, out var layerMarkings))
|
||||
return;
|
||||
|
||||
selected = layerMarkings.Count;
|
||||
}
|
||||
|
||||
public void ChangeMarkingOrder(ProtoId<OrganCategoryPrototype> organ,
|
||||
HumanoidVisualLayers layer,
|
||||
ProtoId<MarkingPrototype> markingId,
|
||||
CandidatePosition position,
|
||||
int positionIndex
|
||||
)
|
||||
{
|
||||
if (!_markings.TryGetValue(organ, out var organMarkings))
|
||||
return;
|
||||
|
||||
if (!organMarkings.TryGetValue(layer, out var layerMarkings))
|
||||
return;
|
||||
|
||||
var currentIndex = layerMarkings.FindIndex(marking => marking.MarkingId == markingId);
|
||||
var currentMarking = layerMarkings[currentIndex];
|
||||
|
||||
if (position == CandidatePosition.Before)
|
||||
{
|
||||
layerMarkings.RemoveAt(currentIndex);
|
||||
var insertionIndex = currentIndex < positionIndex ? positionIndex - 1 : positionIndex;
|
||||
layerMarkings.Insert(insertionIndex, currentMarking);
|
||||
}
|
||||
else if (position == CandidatePosition.After)
|
||||
{
|
||||
layerMarkings.RemoveAt(currentIndex);
|
||||
var insertionIndex = currentIndex > positionIndex ? positionIndex + 1 : positionIndex;
|
||||
layerMarkings.Insert(insertionIndex, currentMarking);
|
||||
}
|
||||
|
||||
MarkingsChanged?.Invoke(organ, layer);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CandidatePosition
|
||||
{
|
||||
Before,
|
||||
After,
|
||||
}
|
||||
3
Content.Client/Humanoid/OrganMarkingPicker.xaml
Normal file
3
Content.Client/Humanoid/OrganMarkingPicker.xaml
Normal file
@@ -0,0 +1,3 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<TabContainer Name="LayerTabs" />
|
||||
</Control>
|
||||
86
Content.Client/Humanoid/OrganMarkingPicker.xaml.cs
Normal file
86
Content.Client/Humanoid/OrganMarkingPicker.xaml.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class OrganMarkingPicker : Control
|
||||
{
|
||||
[Dependency] private readonly MarkingManager _marking = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
private readonly MarkingsViewModel _markingsModel;
|
||||
private readonly HashSet<HumanoidVisualLayers> _layers;
|
||||
private readonly ProtoId<MarkingsGroupPrototype> _group;
|
||||
private readonly ProtoId<OrganCategoryPrototype> _organ;
|
||||
|
||||
public OrganMarkingPicker(MarkingsViewModel markingsModel, ProtoId<OrganCategoryPrototype> organ, HashSet<HumanoidVisualLayers> layers, ProtoId<MarkingsGroupPrototype> group)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_markingsModel = markingsModel;
|
||||
_layers = layers;
|
||||
_group = group;
|
||||
_organ = organ;
|
||||
|
||||
_sprite = _entity.System<SpriteSystem>();
|
||||
|
||||
UpdateMarkings();
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_markingsModel.OrganProfileDataChanged += UpdateMarkings;
|
||||
_markingsModel.EnforcementsChanged += UpdateMarkings;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_markingsModel.OrganProfileDataChanged -= UpdateMarkings;
|
||||
_markingsModel.EnforcementsChanged -= UpdateMarkings;
|
||||
}
|
||||
|
||||
public bool Empty => LayerTabs.ChildCount == 0;
|
||||
|
||||
private void UpdateMarkings()
|
||||
{
|
||||
if (!_markingsModel.OrganProfileData.TryGetValue(_organ, out var organProfileData))
|
||||
return;
|
||||
|
||||
LayerTabs.RemoveAllChildren();
|
||||
var i = 0;
|
||||
foreach (var layer in _layers)
|
||||
{
|
||||
var allMarkings =
|
||||
_markingsModel.EnforceGroupAndSexRestrictions ? _marking.MarkingsByLayerAndGroupAndSex(layer, _group, organProfileData.Sex) : _marking.MarkingsByLayer(layer);
|
||||
|
||||
if (allMarkings.Count == 0)
|
||||
continue;
|
||||
|
||||
var control = new LayerMarkingPicker(_markingsModel, _organ, layer, allMarkings);
|
||||
LayerTabs.AddChild(control);
|
||||
if (Loc.TryGetString($"markings-layer-{layer}-{_group.Id}", out var layerTitle))
|
||||
LayerTabs.SetTabTitle(i, layerTitle);
|
||||
else
|
||||
LayerTabs.SetTabTitle(i, Loc.GetString($"markings-layer-{layer}"));
|
||||
i++;
|
||||
}
|
||||
|
||||
LayerTabs.TabsVisible = i > 1;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<!-- "Slot" selection -->
|
||||
<Label Name="CategoryName" />
|
||||
<BoxContainer Name="SlotSelectorContainer" HorizontalExpand="True">
|
||||
<OptionButton Name="SlotSelector" HorizontalExpand="True" StyleClasses="OpenBoth" />
|
||||
<Button Name="AddButton" Text="{Loc 'marking-slot-add'}" StyleClasses="OpenBoth" />
|
||||
<Button Name="RemoveButton" Text="{Loc 'marking-slot-remove'}" StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
<LineEdit Name="Search" PlaceHolder="{Loc 'markings-search'}" HorizontalExpand="True" />
|
||||
|
||||
<!-- Item list -->
|
||||
<BoxContainer Name="MarkingSelectorContainer" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<ScrollContainer MinHeight="500" VerticalExpand="True" HorizontalExpand="True">
|
||||
<ItemList Name="MarkingList" VerticalExpand="True" />
|
||||
</ScrollContainer>
|
||||
|
||||
<!-- Color sliders -->
|
||||
<ScrollContainer MinHeight="200" HorizontalExpand="True">
|
||||
<BoxContainer Name="ColorSelectorContainer" HorizontalExpand="True" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -1,304 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SingleMarkingPicker : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
/// <summary>
|
||||
/// What happens if a marking is selected.
|
||||
/// It will send the 'slot' (marking index)
|
||||
/// and the selected marking's ID.
|
||||
/// </summary>
|
||||
public Action<(int slot, string id)>? OnMarkingSelect;
|
||||
/// <summary>
|
||||
/// What happens if a slot is removed.
|
||||
/// This will send the 'slot' (marking index).
|
||||
/// </summary>
|
||||
public Action<int>? OnSlotRemove;
|
||||
|
||||
/// <summary>
|
||||
/// What happens when a slot is added.
|
||||
/// </summary>
|
||||
public Action? OnSlotAdd;
|
||||
|
||||
/// <summary>
|
||||
/// What happens if a marking's color is changed.
|
||||
/// Sends a 'slot' number, and the marking in question.
|
||||
/// </summary>
|
||||
public Action<(int slot, Marking marking)>? OnColorChanged;
|
||||
|
||||
// current selected slot
|
||||
private int _slot = -1;
|
||||
private int Slot
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_markings == null || _markings.Count == 0)
|
||||
{
|
||||
_slot = -1;
|
||||
}
|
||||
else if (_slot == -1)
|
||||
{
|
||||
_slot = 0;
|
||||
}
|
||||
|
||||
return _slot;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_markings == null || _markings.Count == 0)
|
||||
{
|
||||
_slot = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
_slot = value;
|
||||
_ignoreItemSelected = true;
|
||||
|
||||
foreach (var item in MarkingList)
|
||||
{
|
||||
item.Selected = (string) item.Metadata! == _markings[_slot].MarkingId;
|
||||
}
|
||||
|
||||
_ignoreItemSelected = false;
|
||||
PopulateColors();
|
||||
}
|
||||
}
|
||||
|
||||
// amount of slots to show
|
||||
private int _totalPoints;
|
||||
|
||||
private bool _ignoreItemSelected;
|
||||
|
||||
private MarkingCategories _category;
|
||||
public MarkingCategories Category
|
||||
{
|
||||
get => _category;
|
||||
set
|
||||
{
|
||||
_category = value;
|
||||
CategoryName.Text = Loc.GetString($"markings-category-{_category}");
|
||||
|
||||
if (!string.IsNullOrEmpty(_species))
|
||||
{
|
||||
PopulateList(Search.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
private IReadOnlyDictionary<string, MarkingPrototype>? _markingPrototypeCache;
|
||||
|
||||
private string? _species;
|
||||
private List<Marking>? _markings;
|
||||
|
||||
private int PointsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_markings == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_totalPoints < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return _totalPoints - _markings.Count;
|
||||
}
|
||||
}
|
||||
|
||||
private int PointsUsed => _markings?.Count ?? 0;
|
||||
|
||||
public SingleMarkingPicker()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
MarkingList.OnItemSelected += SelectMarking;
|
||||
AddButton.OnPressed += _ =>
|
||||
{
|
||||
OnSlotAdd!();
|
||||
};
|
||||
|
||||
SlotSelector.OnItemSelected += args =>
|
||||
{
|
||||
Slot = args.Button.SelectedId;
|
||||
};
|
||||
|
||||
RemoveButton.OnPressed += _ =>
|
||||
{
|
||||
OnSlotRemove!(_slot);
|
||||
};
|
||||
|
||||
Search.OnTextChanged += args =>
|
||||
{
|
||||
PopulateList(args.Text);
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateData(List<Marking> markings, string species, int totalPoints)
|
||||
{
|
||||
_markings = markings;
|
||||
_species = species;
|
||||
_totalPoints = totalPoints;
|
||||
|
||||
_markingPrototypeCache = _markingManager.MarkingsByCategoryAndSpecies(Category, _species);
|
||||
|
||||
Visible = _markingPrototypeCache.Count != 0;
|
||||
if (_markingPrototypeCache.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PopulateList(Search.Text);
|
||||
PopulateColors();
|
||||
PopulateSlotSelector();
|
||||
}
|
||||
|
||||
public void PopulateList(string filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_species))
|
||||
{
|
||||
throw new ArgumentException("Tried to populate marking list without a set species!");
|
||||
}
|
||||
|
||||
_markingPrototypeCache ??= _markingManager.MarkingsByCategoryAndSpecies(Category, _species);
|
||||
|
||||
MarkingSelectorContainer.Visible = _markings != null && _markings.Count != 0;
|
||||
if (_markings == null || _markings.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MarkingList.Clear();
|
||||
|
||||
var sortedMarkings = _markingPrototypeCache.Where(m =>
|
||||
m.Key.ToLower().Contains(filter.ToLower()) ||
|
||||
GetMarkingName(m.Value).ToLower().Contains(filter.ToLower())
|
||||
).OrderBy(p => Loc.GetString($"marking-{p.Key}"));
|
||||
|
||||
foreach (var (id, marking) in sortedMarkings)
|
||||
{
|
||||
var item = MarkingList.AddItem(Loc.GetString($"marking-{id}"), _sprite.Frame0(marking.Sprites[0]));
|
||||
item.Metadata = marking.ID;
|
||||
|
||||
if (_markings[Slot].MarkingId == id)
|
||||
{
|
||||
_ignoreItemSelected = true;
|
||||
item.Selected = true;
|
||||
_ignoreItemSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateColors()
|
||||
{
|
||||
if (_markings == null
|
||||
|| _markings.Count == 0
|
||||
|| !_markingManager.TryGetMarking(_markings[Slot], out var proto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var marking = _markings[Slot];
|
||||
|
||||
ColorSelectorContainer.RemoveAllChildren();
|
||||
|
||||
if (marking.MarkingColors.Count != proto.Sprites.Count)
|
||||
{
|
||||
marking = new Marking(marking.MarkingId, proto.Sprites.Count);
|
||||
}
|
||||
|
||||
for (var i = 0; i < marking.MarkingColors.Count; i++)
|
||||
{
|
||||
var selector = new ColorSelectorSliders
|
||||
{
|
||||
HorizontalExpand = true
|
||||
};
|
||||
selector.Color = marking.MarkingColors[i];
|
||||
selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
|
||||
|
||||
var colorIndex = i;
|
||||
selector.OnColorChanged += color =>
|
||||
{
|
||||
marking.SetColor(colorIndex, color);
|
||||
OnColorChanged!((_slot, marking));
|
||||
};
|
||||
|
||||
ColorSelectorContainer.AddChild(selector);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectMarking(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
if (_ignoreItemSelected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var id = (string) MarkingList[args.ItemIndex].Metadata!;
|
||||
if (!_markingManager.Markings.TryGetValue(id, out var proto))
|
||||
{
|
||||
throw new ArgumentException("Attempted to select non-existent marking.");
|
||||
}
|
||||
|
||||
var oldMarking = _markings![Slot];
|
||||
_markings[Slot] = proto.AsMarking();
|
||||
|
||||
for (var i = 0; i < _markings[Slot].MarkingColors.Count && i < oldMarking.MarkingColors.Count; i++)
|
||||
{
|
||||
_markings[Slot].SetColor(i, oldMarking.MarkingColors[i]);
|
||||
}
|
||||
|
||||
PopulateColors();
|
||||
|
||||
OnMarkingSelect!((_slot, id));
|
||||
}
|
||||
|
||||
// Slot logic
|
||||
|
||||
private void PopulateSlotSelector()
|
||||
{
|
||||
SlotSelector.Visible = Slot >= 0;
|
||||
Search.Visible = Slot >= 0;
|
||||
AddButton.HorizontalExpand = Slot < 0;
|
||||
RemoveButton.HorizontalExpand = Slot < 0;
|
||||
AddButton.Disabled = PointsLeft == 0 && _totalPoints > -1 ;
|
||||
RemoveButton.Disabled = PointsUsed == 0;
|
||||
SlotSelector.Clear();
|
||||
|
||||
if (Slot < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < PointsUsed; i++)
|
||||
{
|
||||
SlotSelector.AddItem(Loc.GetString("marking-slot", ("number", $"{i + 1}")), i);
|
||||
|
||||
if (i == _slot)
|
||||
{
|
||||
SlotSelector.SelectId(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMarkingName(MarkingPrototype marking)
|
||||
{
|
||||
return Loc.GetString($"marking-{marking.ID}");
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,6 @@ public sealed class DragDropHelper<T>
|
||||
_onBeginDrag = onBeginDrag;
|
||||
_onEndDrag = onEndDrag;
|
||||
_onContinueDrag = onContinueDrag;
|
||||
_cfg.OnValueChanged(CCVars.DragDropDeadZone, SetDeadZone, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,6 +89,7 @@ public sealed class DragDropHelper<T>
|
||||
Dragged = target;
|
||||
_state = DragState.MouseDown;
|
||||
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
|
||||
_deadzone = _cfg.GetCVar(CCVars.DragDropDeadZone);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -97,9 +97,9 @@ public sealed class DragDropHelper<T>
|
||||
/// </summary>
|
||||
public void EndDrag()
|
||||
{
|
||||
Dragged = default;
|
||||
_state = DragState.NotDragging;
|
||||
_onEndDrag.Invoke();
|
||||
Dragged = default;
|
||||
}
|
||||
|
||||
private void StartDragging()
|
||||
@@ -143,11 +143,6 @@ public sealed class DragDropHelper<T>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDeadZone(float value)
|
||||
{
|
||||
_deadzone = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Body;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Humanoid;
|
||||
using Content.Client.Inventory;
|
||||
using Content.Client.Lobby.UI;
|
||||
using Content.Client.Players.PlayTimeTracking;
|
||||
@@ -8,7 +8,6 @@ using Content.Client.Station;
|
||||
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.Preferences;
|
||||
@@ -38,7 +37,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly JobRequirementsManager _requirements = default!;
|
||||
[Dependency] private readonly MarkingManager _markings = default!;
|
||||
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[UISystemDependency] private readonly VisualBodySystem _visualBody = default!;
|
||||
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
|
||||
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
|
||||
[UISystemDependency] private readonly GuidebookSystem _guide = default!;
|
||||
@@ -470,16 +469,15 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
}
|
||||
else if (humanoid is not null)
|
||||
{
|
||||
var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
|
||||
var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype;
|
||||
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
|
||||
_visualBody.ApplyProfileTo(dummyEnt, humanoid);
|
||||
}
|
||||
else
|
||||
{
|
||||
dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
|
||||
dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index(HumanoidCharacterProfile.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
|
||||
}
|
||||
|
||||
_humanoid.LoadProfile(dummyEnt, humanoid);
|
||||
|
||||
if (humanoid != null && jobClothes)
|
||||
{
|
||||
DebugTools.Assert(job != null);
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed partial class CharacterPickerButton : ContainerButton
|
||||
|
||||
if (profile is not HumanoidCharacterProfile humanoid)
|
||||
{
|
||||
_previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
|
||||
_previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(HumanoidCharacterProfile.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -98,11 +98,6 @@
|
||||
<Slider HorizontalExpand="True" Name="Skin" MinValue="0" MaxValue="100" Value="20" />
|
||||
<BoxContainer Name="RgbSkinColorContainer" Visible="False" Orientation="Vertical" HorizontalExpand="True"></BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Hair -->
|
||||
<BoxContainer Margin="10" Orientation="Horizontal">
|
||||
<humanoid:SingleMarkingPicker Name="HairStylePicker" Category="Hair" />
|
||||
<humanoid:SingleMarkingPicker Name="FacialHairPicker" Category="FacialHair" />
|
||||
</BoxContainer>
|
||||
<!-- Eyes -->
|
||||
<BoxContainer Margin="10" Orientation="Vertical">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-eyes-label'}" />
|
||||
@@ -132,9 +127,7 @@
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
|
||||
<!-- Markings -->
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<humanoid:MarkingPicker Name="Markings" IgnoreCategories="Hair,FacialHair" />
|
||||
</ScrollContainer>
|
||||
<humanoid:MarkingPicker Name="Markings" HorizontalExpand="True" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.Client.Players.PlayTimeTracking;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.Sprite;
|
||||
using Content.Client.UserInterface.Systems.Guidebook;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -109,6 +110,8 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
private ISawmill _sawmill;
|
||||
|
||||
private MarkingsViewModel _markingsModel = new();
|
||||
|
||||
public HumanoidProfileEditor(
|
||||
IClientPreferencesManager preferencesManager,
|
||||
IConfigurationManager configurationManager,
|
||||
@@ -138,6 +141,8 @@ namespace Content.Client.Lobby.UI
|
||||
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
_allowFlavorText = _cfgManager.GetCVar(CCVars.FlavorText);
|
||||
|
||||
Markings.SetModel(_markingsModel);
|
||||
|
||||
ImportButton.OnPressed += args =>
|
||||
{
|
||||
ImportProfile();
|
||||
@@ -227,7 +232,6 @@ namespace Content.Client.Lobby.UI
|
||||
{
|
||||
SpeciesButton.SelectId(args.Id);
|
||||
SetSpecies(_species[args.Id].ID);
|
||||
UpdateHairPickers();
|
||||
OnSkinColorOnValueChanged();
|
||||
};
|
||||
|
||||
@@ -247,112 +251,6 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hair
|
||||
|
||||
HairStylePicker.OnMarkingSelect += newStyle =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithHairStyleName(newStyle.id));
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
HairStylePicker.OnColorChanged += newColor =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
|
||||
UpdateCMarkingsHair();
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
FacialHairPicker.OnMarkingSelect += newStyle =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
FacialHairPicker.OnColorChanged += newColor =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
|
||||
UpdateCMarkingsFacialHair();
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
HairStylePicker.OnSlotRemove += _ =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
|
||||
);
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsHair();
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
FacialHairPicker.OnSlotRemove += _ =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
|
||||
);
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsFacialHair();
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
HairStylePicker.OnSlotAdd += delegate()
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
|
||||
var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys
|
||||
.FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(hair))
|
||||
return;
|
||||
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithHairStyleName(hair)
|
||||
);
|
||||
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsHair();
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
FacialHairPicker.OnSlotAdd += delegate()
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
|
||||
var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys
|
||||
.FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(hair))
|
||||
return;
|
||||
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithFacialHairStyleName(hair)
|
||||
);
|
||||
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsFacialHair();
|
||||
ReloadPreview();
|
||||
};
|
||||
|
||||
#endregion Hair
|
||||
|
||||
#region SpawnPriority
|
||||
|
||||
foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
|
||||
@@ -376,7 +274,7 @@ namespace Content.Client.Lobby.UI
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithEyeColor(newColor));
|
||||
Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
|
||||
_markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor);
|
||||
ReloadProfilePreview();
|
||||
};
|
||||
|
||||
@@ -418,10 +316,8 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
|
||||
|
||||
Markings.OnMarkingAdded += OnMarkingChange;
|
||||
Markings.OnMarkingRemoved += OnMarkingChange;
|
||||
Markings.OnMarkingColorChange += OnMarkingChange;
|
||||
Markings.OnMarkingRankChange += OnMarkingChange;
|
||||
_markingsModel.MarkingsChanged += (_, _) => OnMarkingChange();
|
||||
_markingsModel.MarkingsReset += OnMarkingChange;
|
||||
|
||||
#endregion Markings
|
||||
|
||||
@@ -626,7 +522,7 @@ namespace Content.Client.Lobby.UI
|
||||
{
|
||||
if (!speciesIds.Contains(Profile.Species))
|
||||
{
|
||||
SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
||||
SetSpecies(HumanoidCharacterProfile.DefaultSpecies);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -768,9 +664,6 @@ namespace Content.Client.Lobby.UI
|
||||
UpdateEyePickers();
|
||||
UpdateSaveButton();
|
||||
UpdateMarkings();
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsHair();
|
||||
UpdateCMarkingsFacialHair();
|
||||
|
||||
RefreshAntags();
|
||||
RefreshJobs();
|
||||
@@ -795,7 +688,7 @@ namespace Content.Client.Lobby.UI
|
||||
if (Profile == null || !_entManager.EntityExists(PreviewDummy))
|
||||
return;
|
||||
|
||||
_entManager.System<HumanoidAppearanceSystem>().LoadProfile(PreviewDummy, Profile);
|
||||
_entManager.System<SharedVisualBodySystem>().ApplyProfileTo(PreviewDummy, Profile);
|
||||
|
||||
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
|
||||
SetDirty();
|
||||
@@ -808,7 +701,7 @@ namespace Content.Client.Lobby.UI
|
||||
// I.e., do what jobs/antags do.
|
||||
|
||||
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
|
||||
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
|
||||
var species = Profile?.Species ?? HumanoidCharacterProfile.DefaultSpecies;
|
||||
var page = DefaultSpeciesGuidebook;
|
||||
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
|
||||
page = new ProtoId<GuideEntryPrototype>(species.Id); // Gross. See above todo comment.
|
||||
@@ -1077,13 +970,14 @@ namespace Content.Client.Lobby.UI
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void OnMarkingChange(MarkingSet markings)
|
||||
private void OnMarkingChange()
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
|
||||
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
|
||||
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(_markingsModel.Markings));
|
||||
ReloadProfilePreview();
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void OnSkinColorOnValueChanged()
|
||||
@@ -1105,7 +999,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
var color = strategy.FromUnary(Skin.Value);
|
||||
|
||||
Markings.CurrentSkinColor = color;
|
||||
_markingsModel.SetOrganSkinColor(color);
|
||||
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
||||
|
||||
break;
|
||||
@@ -1120,7 +1014,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
|
||||
|
||||
Markings.CurrentSkinColor = color;
|
||||
_markingsModel.SetOrganSkinColor(color);
|
||||
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
||||
|
||||
break;
|
||||
@@ -1177,7 +1071,7 @@ namespace Content.Client.Lobby.UI
|
||||
}
|
||||
|
||||
UpdateGenderControls();
|
||||
Markings.SetSex(newSex);
|
||||
_markingsModel.SetOrganSexes(newSex);
|
||||
ReloadPreview();
|
||||
}
|
||||
|
||||
@@ -1191,7 +1085,8 @@ namespace Content.Client.Lobby.UI
|
||||
{
|
||||
Profile = Profile?.WithSpecies(newSpecies);
|
||||
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
|
||||
Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
|
||||
_markingsModel.OrganData = _markingManager.GetMarkingData(newSpecies);
|
||||
_markingsModel.ValidateMarkings();
|
||||
// In case there's job restrictions for the species
|
||||
RefreshJobs();
|
||||
// In case there's species restrictions for loadouts
|
||||
@@ -1358,9 +1253,9 @@ namespace Content.Client.Lobby.UI
|
||||
return;
|
||||
}
|
||||
|
||||
Markings.SetData(Profile.Appearance.Markings, Profile.Species,
|
||||
Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor
|
||||
);
|
||||
_markingsModel.OrganData = _markingManager.GetMarkingData(Profile.Species);
|
||||
_markingsModel.OrganProfileData = _markingManager.GetProfileData(Profile.Species, Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor);
|
||||
_markingsModel.Markings = Profile.Appearance.Markings;
|
||||
}
|
||||
|
||||
private void UpdateGenderControls()
|
||||
@@ -1383,99 +1278,6 @@ namespace Content.Client.Lobby.UI
|
||||
SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
|
||||
}
|
||||
|
||||
private void UpdateHairPickers()
|
||||
{
|
||||
if (Profile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var hairMarking = Profile.Appearance.HairStyleId == HairStyles.DefaultHairStyle
|
||||
? new List<Marking>()
|
||||
: new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) };
|
||||
|
||||
var facialHairMarking = Profile.Appearance.FacialHairStyleId == HairStyles.DefaultFacialHairStyle
|
||||
? new List<Marking>()
|
||||
: new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) };
|
||||
|
||||
HairStylePicker.UpdateData(
|
||||
hairMarking,
|
||||
Profile.Species,
|
||||
1);
|
||||
FacialHairPicker.UpdateData(
|
||||
facialHairMarking,
|
||||
Profile.Species,
|
||||
1);
|
||||
}
|
||||
|
||||
private void UpdateCMarkingsHair()
|
||||
{
|
||||
if (Profile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// hair color
|
||||
Color? hairColor = null;
|
||||
if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle &&
|
||||
_markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto)
|
||||
)
|
||||
{
|
||||
if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, hairProto, _prototypeManager))
|
||||
{
|
||||
if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
|
||||
{
|
||||
hairColor = Profile.Appearance.SkinColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
hairColor = Profile.Appearance.HairColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hairColor != null)
|
||||
{
|
||||
Markings.HairMarking = new (Profile.Appearance.HairStyleId, new List<Color>() { hairColor.Value });
|
||||
}
|
||||
else
|
||||
{
|
||||
Markings.HairMarking = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCMarkingsFacialHair()
|
||||
{
|
||||
if (Profile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// facial hair color
|
||||
Color? facialHairColor = null;
|
||||
if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle &&
|
||||
_markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto))
|
||||
{
|
||||
if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager))
|
||||
{
|
||||
if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
|
||||
{
|
||||
facialHairColor = Profile.Appearance.SkinColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
facialHairColor = Profile.Appearance.FacialHairColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (facialHairColor != null)
|
||||
{
|
||||
Markings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List<Color>() { facialHairColor.Value });
|
||||
}
|
||||
else
|
||||
{
|
||||
Markings.FacialHairMarking = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEyePickers()
|
||||
{
|
||||
if (Profile == null)
|
||||
@@ -1483,7 +1285,7 @@ namespace Content.Client.Lobby.UI
|
||||
return;
|
||||
}
|
||||
|
||||
Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
|
||||
_markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor);
|
||||
EyeColorPicker.SetData(Profile.Appearance.EyeColor);
|
||||
}
|
||||
|
||||
@@ -1542,7 +1344,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
try
|
||||
{
|
||||
var profile = _entManager.System<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
|
||||
var profile = HumanoidCharacterProfile.FromStream(file, _playerManager.LocalSession!);
|
||||
var oldProfile = Profile;
|
||||
SetProfile(profile, CharacterSlot);
|
||||
|
||||
@@ -1574,7 +1376,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
try
|
||||
{
|
||||
var dataNode = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
|
||||
var dataNode = Profile.ToDataNode();
|
||||
await using var writer = new StreamWriter(file.Value.fileStream);
|
||||
dataNode.Write(writer);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Client.Humanoid;
|
||||
using Content.Shared.MagicMirror;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.MagicMirror;
|
||||
@@ -10,6 +9,8 @@ public sealed class MagicMirrorBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private MagicMirrorWindow? _window;
|
||||
|
||||
private readonly MarkingsViewModel _markingsModel = new();
|
||||
|
||||
public MagicMirrorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
@@ -19,49 +20,24 @@ public sealed class MagicMirrorBoundUserInterface : BoundUserInterface
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<MagicMirrorWindow>();
|
||||
_window.MarkingsPicker.SetModel(_markingsModel);
|
||||
|
||||
_window.OnHairSelected += tuple => SelectHair(MagicMirrorCategory.Hair, tuple.id, tuple.slot);
|
||||
_window.OnHairColorChanged += args => ChangeColor(MagicMirrorCategory.Hair, args.marking, args.slot);
|
||||
_window.OnHairSlotAdded += delegate () { AddSlot(MagicMirrorCategory.Hair); };
|
||||
_window.OnHairSlotRemoved += args => RemoveSlot(MagicMirrorCategory.Hair, args);
|
||||
|
||||
_window.OnFacialHairSelected += tuple => SelectHair(MagicMirrorCategory.FacialHair, tuple.id, tuple.slot);
|
||||
_window.OnFacialHairColorChanged +=
|
||||
args => ChangeColor(MagicMirrorCategory.FacialHair, args.marking, args.slot);
|
||||
_window.OnFacialHairSlotAdded += delegate () { AddSlot(MagicMirrorCategory.FacialHair); };
|
||||
_window.OnFacialHairSlotRemoved += args => RemoveSlot(MagicMirrorCategory.FacialHair, args);
|
||||
}
|
||||
|
||||
private void SelectHair(MagicMirrorCategory category, string marking, int slot)
|
||||
{
|
||||
SendMessage(new MagicMirrorSelectMessage(category, marking, slot));
|
||||
}
|
||||
|
||||
private void ChangeColor(MagicMirrorCategory category, Marking marking, int slot)
|
||||
{
|
||||
SendMessage(new MagicMirrorChangeColorMessage(category, new(marking.MarkingColors), slot));
|
||||
}
|
||||
|
||||
private void RemoveSlot(MagicMirrorCategory category, int slot)
|
||||
{
|
||||
SendMessage(new MagicMirrorRemoveSlotMessage(category, slot));
|
||||
}
|
||||
|
||||
private void AddSlot(MagicMirrorCategory category)
|
||||
{
|
||||
SendMessage(new MagicMirrorAddSlotMessage(category));
|
||||
_markingsModel.MarkingsChanged += (_, _) =>
|
||||
{
|
||||
SendMessage(new MagicMirrorSelectMessage(_markingsModel.Markings));
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not MagicMirrorUiState data || _window == null)
|
||||
{
|
||||
if (state is not MagicMirrorUiState data)
|
||||
return;
|
||||
}
|
||||
|
||||
_window.UpdateState(data);
|
||||
_markingsModel.OrganData = data.OrganMarkingData;
|
||||
_markingsModel.OrganProfileData = data.OrganProfileData;
|
||||
_markingsModel.Markings = data.AppliedMarkings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
using Content.Shared.MagicMirror;
|
||||
|
||||
namespace Content.Client.MagicMirror;
|
||||
|
||||
public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
|
||||
Title="{Loc 'magic-mirror-window-title'}"
|
||||
MinSize="600 400">
|
||||
<BoxContainer>
|
||||
<humanoid:SingleMarkingPicker Name="HairPicker" Category="Hair" />
|
||||
<humanoid:SingleMarkingPicker Name="FacialHairPicker" Category="FacialHair" />
|
||||
</BoxContainer>
|
||||
MinSize="700 500">
|
||||
<humanoid:MarkingPicker Name="MarkingsPicker" Access="Public" HorizontalExpand="True" VerticalExpand="True" />
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.MagicMirror;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
@@ -10,40 +7,8 @@ namespace Content.Client.MagicMirror;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MagicMirrorWindow : DefaultWindow
|
||||
{
|
||||
// MMMMMMM
|
||||
public Action<(int slot, string id)>? OnHairSelected;
|
||||
public Action<(int slot, Marking marking)>? OnHairColorChanged;
|
||||
public Action<int>? OnHairSlotRemoved;
|
||||
public Action? OnHairSlotAdded;
|
||||
|
||||
public Action<(int slot, string id)>? OnFacialHairSelected;
|
||||
public Action<(int slot, Marking marking)>? OnFacialHairColorChanged;
|
||||
public Action<int>? OnFacialHairSlotRemoved;
|
||||
public Action? OnFacialHairSlotAdded;
|
||||
|
||||
public MagicMirrorWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
HairPicker.OnMarkingSelect += args => OnHairSelected!(args);
|
||||
HairPicker.OnColorChanged += args => OnHairColorChanged!(args);
|
||||
HairPicker.OnSlotRemove += args => OnHairSlotRemoved!(args);
|
||||
HairPicker.OnSlotAdd += delegate { OnHairSlotAdded!(); };
|
||||
|
||||
FacialHairPicker.OnMarkingSelect += args => OnFacialHairSelected!(args);
|
||||
FacialHairPicker.OnColorChanged += args => OnFacialHairColorChanged!(args);
|
||||
FacialHairPicker.OnSlotRemove += args => OnFacialHairSlotRemoved!(args);
|
||||
FacialHairPicker.OnSlotAdd += delegate { OnFacialHairSlotAdded!(); };
|
||||
}
|
||||
|
||||
public void UpdateState(MagicMirrorUiState state)
|
||||
{
|
||||
HairPicker.UpdateData(state.Hair, state.Species, state.HairSlotTotal);
|
||||
FacialHairPicker.UpdateData(state.FacialHair, state.Species, state.FacialHairSlotTotal);
|
||||
|
||||
if (!HairPicker.Visible && !FacialHairPicker.Visible)
|
||||
{
|
||||
AddChild(new Label { Text = Loc.GetString("magic-mirror-component-activate-user-has-no-hair") });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,18 @@ public sealed class PanelSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet,
|
||||
var boxPositive = new StyleBoxFlat { BackgroundColor = sheet.PositivePalette.Background };
|
||||
var boxNegative = new StyleBoxFlat { BackgroundColor = sheet.NegativePalette.Background };
|
||||
var boxHighlight = new StyleBoxFlat { BackgroundColor = sheet.HighlightPalette.Background };
|
||||
var boxDropTarget = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = sheet.ButtonPalette.BackgroundDark.WithAlpha(0.5f),
|
||||
BorderColor = sheet.ButtonPalette.Base,
|
||||
BorderThickness = new(2)
|
||||
};
|
||||
|
||||
return
|
||||
[
|
||||
E<PanelContainer>().Class(StyleClass.PanelLight).Panel(boxLight),
|
||||
E<PanelContainer>().Class(StyleClass.PanelDark).Panel(boxDark),
|
||||
E<PanelContainer>().Class(StyleClass.PanelDropTarget).Panel(boxDropTarget),
|
||||
|
||||
E<PanelContainer>().Class(StyleClass.Positive).Panel(boxPositive),
|
||||
E<PanelContainer>().Class(StyleClass.Negative).Panel(boxNegative),
|
||||
|
||||
@@ -51,6 +51,7 @@ public static class StyleClass
|
||||
|
||||
public const string PanelDark = "PanelDark";
|
||||
public const string PanelLight = "PanelLight";
|
||||
public const string PanelDropTarget = "PanelDropTarget";
|
||||
|
||||
public const string ButtonOpenRight = "OpenRight";
|
||||
public const string ButtonOpenLeft = "OpenLeft";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Content.Shared.Zombies;
|
||||
@@ -40,7 +40,7 @@ public sealed class ZombieSystem : SharedZombieSystem
|
||||
|
||||
private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args)
|
||||
{
|
||||
if (HasComp<HumanoidAppearanceComponent>(uid))
|
||||
if (HasComp<VisualBodyComponent>(uid))
|
||||
return;
|
||||
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
|
||||
@@ -110,10 +110,6 @@ public sealed class CharacterCreationTest
|
||||
if (a.MemberwiseEquals(b))
|
||||
return;
|
||||
|
||||
Assert.That(a.HairStyleId, Is.EqualTo(b.HairStyleId));
|
||||
Assert.That(a.HairColor, Is.EqualTo(b.HairColor));
|
||||
Assert.That(a.FacialHairStyleId, Is.EqualTo(b.FacialHairStyleId));
|
||||
Assert.That(a.FacialHairColor, Is.EqualTo(b.FacialHairColor));
|
||||
Assert.That(a.EyeColor, Is.EqualTo(b.EyeColor));
|
||||
Assert.That(a.SkinColor, Is.EqualTo(b.SkinColor));
|
||||
Assert.That(a.Markings, Is.EquivalentTo(b.Markings));
|
||||
|
||||
238
Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs
Normal file
238
Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Markings;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(MarkingManager))]
|
||||
public sealed class MarkingManagerTests
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: markingsGroup
|
||||
id: Testing
|
||||
|
||||
- type: markingsGroup
|
||||
id: TestingOther
|
||||
|
||||
- type: markingsGroup
|
||||
id: TestingOptionalEyes
|
||||
limits:
|
||||
enum.HumanoidVisualLayers.Eyes:
|
||||
limit: 1
|
||||
required: false
|
||||
|
||||
- type: markingsGroup
|
||||
id: TestingRequiredEyes
|
||||
limits:
|
||||
enum.HumanoidVisualLayers.Eyes:
|
||||
limit: 1
|
||||
required: true
|
||||
default: [ EyesMarking ]
|
||||
|
||||
- type: marking
|
||||
id: SingleColorMarking
|
||||
bodyPart: Eyes
|
||||
sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
|
||||
coloring:
|
||||
default:
|
||||
type:
|
||||
!type:EyeColoring
|
||||
|
||||
- type: marking
|
||||
id: MenOnlyMarking
|
||||
bodyPart: Eyes
|
||||
sexRestriction: Male
|
||||
sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
|
||||
|
||||
- type: marking
|
||||
id: TestingOnlyMarking
|
||||
bodyPart: Eyes
|
||||
groupWhitelist: [ Testing ]
|
||||
sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
|
||||
|
||||
- type: marking
|
||||
id: TestingMenOnlyMarking
|
||||
bodyPart: Eyes
|
||||
sexRestriction: Male
|
||||
groupWhitelist: [ Testing ]
|
||||
sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
|
||||
|
||||
- type: marking
|
||||
id: EyesMarking
|
||||
bodyPart: Eyes
|
||||
sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
|
||||
|
||||
- type: marking
|
||||
id: ChestMarking
|
||||
bodyPart: Chest
|
||||
sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task HairConvesion()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var markingManager = server.ResolveDependency<MarkingManager>();
|
||||
|
||||
var markings = new List<Marking>() { new("HumanHairLongBedhead2", new List<Color>() { Color.Red }) };
|
||||
|
||||
var converted = markingManager.ConvertMarkings(markings, "Human");
|
||||
|
||||
Assert.That(converted, Does.ContainKey(new ProtoId<OrganCategoryPrototype>("Head")));
|
||||
Assert.That(converted["Head"], Does.ContainKey(HumanoidVisualLayers.Hair));
|
||||
var hairMarkings = converted["Head"][HumanoidVisualLayers.Hair];
|
||||
Assert.That(hairMarkings, Has.Count.EqualTo(1));
|
||||
Assert.That(hairMarkings[0].MarkingId, Is.EqualTo("HumanHairLongBedhead2"));
|
||||
Assert.That(hairMarkings[0].MarkingColors[0], Is.EqualTo(Color.Red));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LimitsFilling()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var markingManager = server.ResolveDependency<MarkingManager>();
|
||||
var dict = new Dictionary<HumanoidVisualLayers, List<Marking>>();
|
||||
|
||||
markingManager.EnsureValidLimits(dict, "TestingRequiredEyes", new() { HumanoidVisualLayers.Eyes }, null, null);
|
||||
Assert.That(dict, Does.ContainKey(HumanoidVisualLayers.Eyes));
|
||||
Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
|
||||
Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("EyesMarking"));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LimitsTruncations()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var markingManager = server.ResolveDependency<MarkingManager>();
|
||||
var dict = new Dictionary<HumanoidVisualLayers, List<Marking>>()
|
||||
{
|
||||
[HumanoidVisualLayers.Eyes] = new()
|
||||
{
|
||||
new("EyesMarking", 0),
|
||||
new("MenOnlyMarking", 0),
|
||||
},
|
||||
};
|
||||
|
||||
markingManager.EnsureValidLimits(dict, "TestingOptionalEyes", new() { HumanoidVisualLayers.Eyes }, null, null);
|
||||
Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
|
||||
Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking"));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EnsureValidGroupAndSex()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var markingManager = server.ResolveDependency<MarkingManager>();
|
||||
var dictFactory = static () => new Dictionary<HumanoidVisualLayers, List<Marking>>()
|
||||
{
|
||||
[HumanoidVisualLayers.Eyes] = new()
|
||||
{
|
||||
new("MenOnlyMarking", 0),
|
||||
new("TestingOnlyMarking", 0),
|
||||
new("TestingMenOnlyMarking", 0),
|
||||
}
|
||||
};
|
||||
|
||||
var menMarkings = dictFactory();
|
||||
markingManager.EnsureValidGroupAndSex(menMarkings, "TestingOther", Sex.Male);
|
||||
|
||||
Assert.That(menMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
|
||||
Assert.That(menMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking"));
|
||||
|
||||
var testingMarkings = dictFactory();
|
||||
markingManager.EnsureValidGroupAndSex(testingMarkings, "Testing", Sex.Female);
|
||||
|
||||
Assert.That(testingMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
|
||||
Assert.That(testingMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("TestingOnlyMarking"));
|
||||
|
||||
var testingMenMarkings = dictFactory();
|
||||
markingManager.EnsureValidGroupAndSex(testingMenMarkings, "Testing", Sex.Male);
|
||||
|
||||
Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(3));
|
||||
Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking"));
|
||||
Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][1].MarkingId, Is.EqualTo("TestingOnlyMarking"));
|
||||
Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][2].MarkingId, Is.EqualTo("TestingMenOnlyMarking"));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EnsureValidColors()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var markingManager = server.ResolveDependency<MarkingManager>();
|
||||
|
||||
var dict = new Dictionary<HumanoidVisualLayers, List<Marking>>()
|
||||
{
|
||||
[HumanoidVisualLayers.Eyes] = new()
|
||||
{
|
||||
new("SingleColorMarking", 0),
|
||||
new("SingleColorMarking", new List<Color>() { Color.Red }),
|
||||
new("SingleColorMarking", 2),
|
||||
new("SingleColorMarking", new List<Color>() { Color.Green }),
|
||||
}
|
||||
};
|
||||
|
||||
markingManager.EnsureValidColors(dict);
|
||||
|
||||
var eyeMarkings = dict[HumanoidVisualLayers.Eyes];
|
||||
|
||||
// ensure all colors are the correct length
|
||||
Assert.That(eyeMarkings[0].MarkingColors, Has.Count.EqualTo(1));
|
||||
Assert.That(eyeMarkings[1].MarkingColors, Has.Count.EqualTo(1));
|
||||
Assert.That(eyeMarkings[2].MarkingColors, Has.Count.EqualTo(1));
|
||||
Assert.That(eyeMarkings[3].MarkingColors, Has.Count.EqualTo(1));
|
||||
|
||||
// and make sure we didn't shuffle our colors around
|
||||
Assert.That(eyeMarkings[1].MarkingColors[0], Is.EqualTo(Color.Red));
|
||||
Assert.That(eyeMarkings[3].MarkingColors[0], Is.EqualTo(Color.Green));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Preferences.Loadouts.Effects;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Preferences
|
||||
@@ -46,10 +48,6 @@ namespace Content.IntegrationTests.Tests.Preferences
|
||||
Species = "Human",
|
||||
Age = 21,
|
||||
Appearance = new(
|
||||
"Afro",
|
||||
Color.Aqua,
|
||||
"Shaved",
|
||||
Color.Aquamarine,
|
||||
Color.Azure,
|
||||
Color.Beige,
|
||||
new ())
|
||||
@@ -59,12 +57,14 @@ namespace Content.IntegrationTests.Tests.Preferences
|
||||
private static ServerDbSqlite GetDb(RobustIntegrationTest.ServerIntegrationInstance server)
|
||||
{
|
||||
var cfg = server.ResolveDependency<IConfigurationManager>();
|
||||
var serialization = server.ResolveDependency<ISerializationManager>();
|
||||
var task = server.ResolveDependency<ITaskManager>();
|
||||
var opsLog = server.ResolveDependency<ILogManager>().GetSawmill("db.ops");
|
||||
var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
|
||||
var conn = new SqliteConnection("Data Source=:memory:");
|
||||
conn.Open();
|
||||
builder.UseSqlite(conn);
|
||||
return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog);
|
||||
return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog, task, serialization);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
2125
Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs
generated
Normal file
2125
Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class OrganMarkings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<JsonDocument>(
|
||||
name: "organ_markings",
|
||||
table: "profile",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "organ_markings",
|
||||
table: "profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Content.Server.Database;
|
||||
@@ -20,7 +21,7 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.1")
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -795,7 +796,7 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("admin_ooc_color");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ConstructionFavorites")
|
||||
b.PrimitiveCollection<List<string>>("ConstructionFavorites")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("construction_favorites");
|
||||
@@ -874,6 +875,10 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("markings");
|
||||
|
||||
b.Property<JsonDocument>("OrganMarkings")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("organ_markings");
|
||||
|
||||
b.Property<int>("PreferenceId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("preference_id");
|
||||
|
||||
2048
Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs
generated
Normal file
2048
Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class OrganMarkings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "organ_markings",
|
||||
table: "profile",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "organ_markings",
|
||||
table: "profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Admin", b =>
|
||||
{
|
||||
@@ -826,6 +826,10 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("markings");
|
||||
|
||||
b.Property<byte[]>("OrganMarkings")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("organ_markings");
|
||||
|
||||
b.Property<int>("PreferenceId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("preference_id");
|
||||
|
||||
@@ -406,6 +406,7 @@ namespace Content.Server.Database
|
||||
public string Sex { get; set; } = null!;
|
||||
public string Gender { get; set; } = null!;
|
||||
public string Species { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public JsonDocument? OrganMarkings { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
|
||||
public string HairName { get; set; } = null!;
|
||||
public string HairColor { get; set; } = null!;
|
||||
|
||||
@@ -85,6 +85,10 @@ namespace Content.Server.Database
|
||||
.Property(log => log.Markings)
|
||||
.HasConversion(jsonByteArrayConverter);
|
||||
|
||||
modelBuilder.Entity<Profile>()
|
||||
.Property(log => log.OrganMarkings)
|
||||
.HasConversion(jsonByteArrayConverter);
|
||||
|
||||
// EF core can make this automatically unique on sqlite but not psql.
|
||||
modelBuilder.Entity<IPIntelCache>()
|
||||
.HasIndex(p => p.Address)
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed partial class AdminVerbSystem
|
||||
};
|
||||
args.Verbs.Add(ninja);
|
||||
|
||||
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
|
||||
if (HasComp<HumanoidProfileComponent>(args.Target)) // only humanoids can be cloned
|
||||
args.Verbs.Add(paradox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +585,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (_arrivals.IsOnArrivals((entity.Value, null)))
|
||||
return false;
|
||||
|
||||
if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity))
|
||||
if (!def.AllowNonHumans && !HasComp<HumanoidProfileComponent>(entity))
|
||||
return false;
|
||||
|
||||
if (def.Whitelist != null)
|
||||
|
||||
5
Content.Server/Body/VisualBodySystem.cs
Normal file
5
Content.Server/Body/VisualBodySystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Body;
|
||||
|
||||
namespace Content.Server.Body;
|
||||
|
||||
public sealed partial class VisualBodySystem : SharedVisualBodySystem;
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Database;
|
||||
@@ -27,7 +28,6 @@ namespace Content.Server.Cloning;
|
||||
/// </summary>
|
||||
public sealed partial class CloningSystem : SharedCloningSystem
|
||||
{
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
@@ -36,6 +36,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
||||
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; //TODO: This system has to support both the old and new status effect systems, until the old is able to be fully removed.
|
||||
|
||||
@@ -48,7 +49,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
|
||||
if (!_prototype.Resolve(settingsId, out var settings))
|
||||
return false; // invalid settings
|
||||
|
||||
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
|
||||
if (!TryComp<HumanoidProfileComponent>(original, out var humanoid))
|
||||
return false; // whatever body was to be cloned, was not a humanoid
|
||||
|
||||
if (!_prototype.Resolve(humanoid.Species, out var speciesPrototype))
|
||||
@@ -60,7 +61,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
|
||||
return false; // cannot clone, for example due to the unrevivable trait
|
||||
|
||||
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
|
||||
_humanoidSystem.CloneAppearance(original, clone.Value);
|
||||
_visualBody.CopyAppearanceFrom(original, clone.Value);
|
||||
|
||||
CloneComponents(original, clone.Value, settings);
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ public sealed class OutfitSystem : EntitySystem
|
||||
break;
|
||||
|
||||
// Don't require a player, so this works on Urists
|
||||
profile ??= EntityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
|
||||
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
|
||||
profile ??= EntityManager.TryGetComponent<HumanoidProfileComponent>(target, out var comp)
|
||||
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species, comp.Sex)
|
||||
: new HumanoidCharacterProfile();
|
||||
// Try to get the user's existing loadout for the role
|
||||
profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
|
||||
|
||||
61
Content.Server/Database/DataNodeJsonExtensions.cs
Normal file
61
Content.Server/Database/DataNodeJsonExtensions.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
public static class DataNodeJsonExtensions
|
||||
{
|
||||
private static JsonNode ToJsonNode(this MappingDataNode node)
|
||||
{
|
||||
return new JsonObject(node.Children.Select(kvp => new KeyValuePair<string, JsonNode?>(kvp.Key, kvp.Value.ToJsonNode())));
|
||||
}
|
||||
|
||||
private static JsonNode ToJsonNode(this SequenceDataNode node)
|
||||
{
|
||||
return new JsonArray(node.Select(ToJsonNode).ToArray());
|
||||
}
|
||||
|
||||
public static JsonNode? ToJsonNode(this DataNode node)
|
||||
{
|
||||
return node switch
|
||||
{
|
||||
ValueDataNode valueDataNode => JsonValue.Create(valueDataNode.IsNull ? null : valueDataNode.Value),
|
||||
MappingDataNode mappingDataNode => mappingDataNode.ToJsonNode(),
|
||||
SequenceDataNode sequenceNode => sequenceNode.ToJsonNode(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||
};
|
||||
}
|
||||
|
||||
public static DataNode ToDataNode(this JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => new MappingDataNode(element.EnumerateObject().ToDictionary(kvp => kvp.Name, kvp => kvp.Value.ToDataNode())),
|
||||
JsonValueKind.Array => new SequenceDataNode(element.EnumerateArray().Select(item => item.ToDataNode()).ToList()),
|
||||
JsonValueKind.Number => new ValueDataNode(element.GetRawText()),
|
||||
JsonValueKind.String => new ValueDataNode(element.GetString()),
|
||||
JsonValueKind.True => new ValueDataNode("true"),
|
||||
JsonValueKind.False => new ValueDataNode("false"),
|
||||
JsonValueKind.Null => new ValueDataNode("null"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(element)),
|
||||
};
|
||||
}
|
||||
|
||||
public static DataNode ToDataNode(this JsonNode? node)
|
||||
{
|
||||
return node switch
|
||||
{
|
||||
null => ValueDataNode.Null(),
|
||||
JsonValue value => new ValueDataNode(value.GetValue<string>()),
|
||||
JsonArray array => new SequenceDataNode(array.Select(item => item.ToDataNode()).ToList()),
|
||||
JsonObject obj => new MappingDataNode(obj.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToDataNode())),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
@@ -18,9 +19,11 @@ using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traits;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Database
|
||||
@@ -29,10 +32,14 @@ namespace Content.Server.Database
|
||||
{
|
||||
private readonly ISawmill _opsLog;
|
||||
public event Action<DatabaseNotification>? OnNotificationReceived;
|
||||
private readonly ITaskManager _task;
|
||||
private readonly ISerializationManager _serialization;
|
||||
|
||||
/// <param name="opsLog">Sawmill to trace log database operations to.</param>
|
||||
public ServerDbBase(ISawmill opsLog)
|
||||
public ServerDbBase(ISawmill opsLog, ITaskManager taskManager, ISerializationManager serialization)
|
||||
{
|
||||
_task = taskManager;
|
||||
_serialization = serialization;
|
||||
_opsLog = opsLog;
|
||||
}
|
||||
|
||||
@@ -62,7 +69,7 @@ namespace Content.Server.Database
|
||||
var profiles = new Dictionary<int, ICharacterProfile>(maxSlot);
|
||||
foreach (var profile in prefs.Profiles)
|
||||
{
|
||||
profiles[profile.Slot] = ConvertProfiles(profile);
|
||||
profiles[profile.Slot] = await ConvertProfiles(profile);
|
||||
}
|
||||
|
||||
var constructionFavorites = new List<ProtoId<ConstructionPrototype>>(prefs.ConstructionFavorites.Count);
|
||||
@@ -202,8 +209,21 @@ namespace Content.Server.Database
|
||||
prefs.SelectedCharacterSlot = newSlot;
|
||||
}
|
||||
|
||||
private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
|
||||
private static TValue? TryDeserialize<TValue>(JsonDocument document) where TValue : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return document.Deserialize<TValue>();
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HumanoidCharacterProfile> ConvertProfiles(Profile profile)
|
||||
{
|
||||
|
||||
var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
|
||||
var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
|
||||
var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
|
||||
@@ -218,20 +238,53 @@ namespace Content.Server.Database
|
||||
if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
|
||||
gender = genderVal;
|
||||
|
||||
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
||||
var markingsRaw = profile.Markings?.Deserialize<List<string>>();
|
||||
|
||||
List<Marking> markings = new();
|
||||
if (markingsRaw != null)
|
||||
var markings =
|
||||
new Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>();
|
||||
|
||||
if (profile.OrganMarkings?.RootElement is { } element)
|
||||
{
|
||||
var data = element.ToDataNode();
|
||||
markings = _serialization
|
||||
.Read<Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>>(
|
||||
data,
|
||||
notNullableOverride: true);
|
||||
}
|
||||
else if (profile.Markings is { } profileMarkings && TryDeserialize<List<string>>(profileMarkings) is { } markingsRaw)
|
||||
{
|
||||
List<Marking> markingsList = new();
|
||||
|
||||
foreach (var marking in markingsRaw)
|
||||
{
|
||||
var parsed = Marking.ParseFromDbString(marking);
|
||||
|
||||
if (parsed is null) continue;
|
||||
|
||||
markings.Add(parsed);
|
||||
markingsList.Add(parsed);
|
||||
}
|
||||
|
||||
if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } facialMarking)
|
||||
markingsList.Add(facialMarking);
|
||||
|
||||
if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } hairMarking)
|
||||
markingsList.Add(hairMarking);
|
||||
|
||||
var completion = new TaskCompletionSource();
|
||||
_task.RunOnMainThread(() =>
|
||||
{
|
||||
var markingManager = IoCManager.Resolve<MarkingManager>();
|
||||
|
||||
try
|
||||
{
|
||||
markings = markingManager.ConvertMarkings(markingsList, profile.Species);
|
||||
completion.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completion.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
await completion.Task;
|
||||
}
|
||||
|
||||
var loadouts = new Dictionary<string, RoleLoadout>();
|
||||
@@ -267,10 +320,6 @@ namespace Content.Server.Database
|
||||
gender,
|
||||
new HumanoidCharacterAppearance
|
||||
(
|
||||
profile.HairName,
|
||||
Color.FromHex(profile.HairColor),
|
||||
profile.FacialHairName,
|
||||
Color.FromHex(profile.FacialHairColor),
|
||||
Color.FromHex(profile.EyeColor),
|
||||
Color.FromHex(profile.SkinColor),
|
||||
markings
|
||||
@@ -284,16 +333,11 @@ namespace Content.Server.Database
|
||||
);
|
||||
}
|
||||
|
||||
private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
|
||||
private Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
|
||||
{
|
||||
profile ??= new Profile();
|
||||
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
|
||||
List<string> markingStrings = new();
|
||||
foreach (var marking in appearance.Markings)
|
||||
{
|
||||
markingStrings.Add(marking.ToString());
|
||||
}
|
||||
var markings = JsonSerializer.SerializeToDocument(markingStrings);
|
||||
var dataNode = _serialization.WriteValue(appearance.Markings, alwaysWrite: true, notNullableOverride: true);
|
||||
|
||||
profile.CharacterName = humanoid.Name;
|
||||
profile.FlavorText = humanoid.FlavorText;
|
||||
@@ -301,14 +345,28 @@ namespace Content.Server.Database
|
||||
profile.Age = humanoid.Age;
|
||||
profile.Sex = humanoid.Sex.ToString();
|
||||
profile.Gender = humanoid.Gender.ToString();
|
||||
profile.HairName = appearance.HairStyleId;
|
||||
profile.HairColor = appearance.HairColor.ToHex();
|
||||
profile.FacialHairName = appearance.FacialHairStyleId;
|
||||
profile.FacialHairColor = appearance.FacialHairColor.ToHex();
|
||||
profile.EyeColor = appearance.EyeColor.ToHex();
|
||||
profile.SkinColor = appearance.SkinColor.ToHex();
|
||||
profile.SpawnPriority = (int) humanoid.SpawnPriority;
|
||||
profile.Markings = markings;
|
||||
profile.OrganMarkings = JsonSerializer.SerializeToDocument(dataNode.ToJsonNode());
|
||||
|
||||
// support for downgrades - at some point this should be removed
|
||||
var legacyMarkings = appearance.Markings
|
||||
.SelectMany(organ => organ.Value.Values)
|
||||
.SelectMany(i => i)
|
||||
.Select(marking => marking.ToString())
|
||||
.ToList();
|
||||
var flattenedMarkings = appearance.Markings.SelectMany(it => it.Value)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
var hairMarking = flattenedMarkings.FirstOrNull(kvp => kvp.Key == HumanoidVisualLayers.Hair)?.Value.FirstOrDefault();
|
||||
var facialHairMarking = flattenedMarkings.FirstOrNull(kvp => kvp.Key == HumanoidVisualLayers.FacialHair)?.Value.FirstOrDefault();
|
||||
profile.Markings =
|
||||
JsonSerializer.SerializeToDocument(legacyMarkings.Select(marking => marking.ToString()).ToList());
|
||||
profile.HairName = hairMarking?.MarkingId ?? HairStyles.DefaultHairStyle;
|
||||
profile.FacialHairName = facialHairMarking?.MarkingId ?? HairStyles.DefaultFacialHairStyle;
|
||||
profile.HairColor = (hairMarking?.MarkingColors[0] ?? Color.Black).ToHex();
|
||||
profile.FacialHairColor = (facialHairMarking?.MarkingColors[0] ?? Color.Black).ToHex();
|
||||
|
||||
profile.Slot = slot;
|
||||
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using Prometheus;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using LogLevel = Robust.Shared.Log.LogLevel;
|
||||
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
@@ -31,6 +33,8 @@ namespace Content.Server.Database
|
||||
|
||||
void Shutdown();
|
||||
|
||||
Task<bool> HasPendingModelChanges();
|
||||
|
||||
#region Preferences
|
||||
Task<PlayerPreferences> InitPrefsAsync(
|
||||
NetUserId userId,
|
||||
@@ -407,6 +411,8 @@ namespace Content.Server.Database
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ILogManager _logMgr = default!;
|
||||
[Dependency] private readonly ITaskManager _task = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
|
||||
private ServerDbBase _db = default!;
|
||||
private LoggingProvider _msLogProvider = default!;
|
||||
@@ -438,11 +444,11 @@ namespace Content.Server.Database
|
||||
{
|
||||
case "sqlite":
|
||||
SetupSqlite(out var contextFunc, out var inMemory);
|
||||
_db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
|
||||
_db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog, _task, _serialization);
|
||||
break;
|
||||
case "postgres":
|
||||
var (pgOptions, conString) = CreatePostgresOptions();
|
||||
_db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
|
||||
_db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog, _task, _serialization);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException($"Unknown database engine {engine}.");
|
||||
@@ -1082,6 +1088,11 @@ namespace Content.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> HasPendingModelChanges()
|
||||
{
|
||||
return RunDbCommand(() => _db.HasPendingModelChanges());
|
||||
}
|
||||
|
||||
// Wrapper functions to run DB commands from the thread pool.
|
||||
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
|
||||
// For SQLite, this will also enable read parallelization (within limits).
|
||||
|
||||
@@ -11,8 +11,10 @@ using Content.Server.IP;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Database
|
||||
@@ -30,8 +32,10 @@ namespace Content.Server.Database
|
||||
string connectionString,
|
||||
IConfigurationManager cfg,
|
||||
ISawmill opsLog,
|
||||
ISawmill notifyLog)
|
||||
: base(opsLog)
|
||||
ISawmill notifyLog,
|
||||
ITaskManager taskManager,
|
||||
ISerializationManager serialization)
|
||||
: base(opsLog, taskManager, serialization)
|
||||
{
|
||||
var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency);
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ using Content.Server.Preferences.Managers;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Database
|
||||
@@ -36,8 +38,10 @@ namespace Content.Server.Database
|
||||
bool inMemory,
|
||||
IConfigurationManager cfg,
|
||||
bool synchronous,
|
||||
ISawmill opsLog)
|
||||
: base(opsLog)
|
||||
ISawmill opsLog,
|
||||
ITaskManager taskManager,
|
||||
ISerializationManager serialization)
|
||||
: base(opsLog, taskManager, serialization)
|
||||
{
|
||||
_options = options;
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace Content.Server.Destructible
|
||||
}));
|
||||
|
||||
// If it doesn't have a humanoid component, it's probably not particularly notable?
|
||||
if (logImpact > LogImpact.Medium && !HasComp<HumanoidAppearanceComponent>(uid))
|
||||
if (logImpact > LogImpact.Medium && !HasComp<HumanoidProfileComponent>(uid))
|
||||
logImpact = LogImpact.Medium;
|
||||
|
||||
if (args.Origin != null)
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
|
||||
speciesId = roundStart.Count == 0
|
||||
? SharedHumanoidAppearanceSystem.DefaultSpecies
|
||||
? HumanoidCharacterProfile.DefaultSpecies
|
||||
: _robustRandom.Pick(roundStart);
|
||||
}
|
||||
else
|
||||
@@ -211,6 +211,7 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
|
||||
character = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
|
||||
character.Appearance = HumanoidCharacterAppearance.EnsureValid(character.Appearance, character.Species, character.Sex);
|
||||
}
|
||||
|
||||
// We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
@@ -11,9 +12,10 @@ namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfileRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -34,7 +36,7 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
|
||||
|
||||
if (profile?.Species is not { } speciesId || !_proto.Resolve(speciesId, out var species))
|
||||
{
|
||||
species = _proto.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
||||
species = _proto.Index<SpeciesPrototype>(HumanoidCharacterProfile.DefaultSpecies);
|
||||
}
|
||||
|
||||
if (ent.Comp.SpeciesOverride != null
|
||||
@@ -44,6 +46,10 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
|
||||
}
|
||||
|
||||
args.Entity = Spawn(species.Prototype);
|
||||
_humanoid.LoadProfile(args.Entity.Value, profile?.WithSpecies(species.ID));
|
||||
if (profile?.WithSpecies(species.ID) is { } humanoidProfile)
|
||||
{
|
||||
_visualBody.ApplyProfileTo(args.Entity.Value, humanoidProfile);
|
||||
_humanoidProfile.ApplyProfileTo(args.Entity.Value, humanoidProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
|
||||
if (HasComp<RevolutionaryComponent>(ev.Target) ||
|
||||
HasComp<MindShieldComponent>(ev.Target) ||
|
||||
!HasComp<HumanoidAppearanceComponent>(ev.Target) &&
|
||||
!HasComp<HumanoidProfileComponent>(ev.Target) &&
|
||||
!alwaysConvertible ||
|
||||
!_mobState.IsAlive(ev.Target) ||
|
||||
HasComp<ZombieComponent>(ev.Target))
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||
|
||||
private string MakeBriefing(EntityUid ent)
|
||||
{
|
||||
var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
|
||||
var isHuman = HasComp<HumanoidProfileComponent>(ent);
|
||||
var briefing = isHuman
|
||||
? Loc.GetString("thief-role-greeting-human")
|
||||
: Loc.GetString("thief-role-greeting-animal");
|
||||
|
||||
@@ -166,7 +166,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
{
|
||||
var players = GetHealthyHumans(includeOffStation);
|
||||
var zombieCount = 0;
|
||||
var query = EntityQueryEnumerator<HumanoidAppearanceComponent, ZombieComponent, MobStateComponent>();
|
||||
var query = EntityQueryEnumerator<HumanoidProfileComponent, ZombieComponent, MobStateComponent>();
|
||||
while (query.MoveNext(out _, out _, out _, out var mob))
|
||||
{
|
||||
if (!includeDead && mob.CurrentState == MobState.Dead)
|
||||
@@ -196,7 +196,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
}
|
||||
}
|
||||
|
||||
var players = AllEntityQuery<HumanoidAppearanceComponent, ActorComponent, MobStateComponent, TransformComponent>();
|
||||
var players = AllEntityQuery<HumanoidProfileComponent, ActorComponent, MobStateComponent, TransformComponent>();
|
||||
var zombers = GetEntityQuery<ZombieComponent>();
|
||||
while (players.MoveNext(out var uid, out _, out _, out var mob, out var xform))
|
||||
{
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Server.CharacterAppearance.Components;
|
||||
namespace Content.Server.Humanoid.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class RandomHumanoidAppearanceComponent : Component
|
||||
{
|
||||
[DataField("randomizeName")] public bool RandomizeName = true;
|
||||
/// <summary>
|
||||
/// After randomizing, sets the hair style to this, if possible
|
||||
/// </summary>
|
||||
[DataField] public string? Hair = null;
|
||||
[DataField]
|
||||
public bool RandomizeName = true;
|
||||
}
|
||||
|
||||
5
Content.Server/Humanoid/HideableHumanoidLayersSystem.cs
Normal file
5
Content.Server/Humanoid/HideableHumanoidLayersSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Humanoid;
|
||||
|
||||
namespace Content.Server.Humanoid;
|
||||
|
||||
public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem;
|
||||
@@ -1,104 +0,0 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Humanoid;
|
||||
|
||||
public sealed partial class HumanoidAppearanceSystem
|
||||
{
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
private void OnVerbsRequest(EntityUid uid, HumanoidAppearanceComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_adminManager.HasAdminFlag(actor.PlayerSession, AdminFlags.Fun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Verbs.Add(new Verb
|
||||
{
|
||||
Text = "Modify markings",
|
||||
Category = VerbCategory.Tricks,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"),
|
||||
Act = () =>
|
||||
{
|
||||
_uiSystem.OpenUi(uid, HumanoidMarkingModifierKey.Key, actor.PlayerSession);
|
||||
_uiSystem.SetUiState(
|
||||
uid,
|
||||
HumanoidMarkingModifierKey.Key,
|
||||
new HumanoidMarkingModifierState(component.MarkingSet, component.Species,
|
||||
component.Sex,
|
||||
component.SkinColor,
|
||||
component.CustomBaseLayers
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnBaseLayersSet(EntityUid uid, HumanoidAppearanceComponent component,
|
||||
HumanoidMarkingModifierBaseLayersSetMessage message)
|
||||
{
|
||||
if (!_adminManager.HasAdminFlag(message.Actor, AdminFlags.Fun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Info == null)
|
||||
{
|
||||
component.CustomBaseLayers.Remove(message.Layer);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.CustomBaseLayers[message.Layer] = message.Info.Value;
|
||||
}
|
||||
|
||||
Dirty(uid, component);
|
||||
|
||||
if (message.ResendState)
|
||||
{
|
||||
_uiSystem.SetUiState(
|
||||
uid,
|
||||
HumanoidMarkingModifierKey.Key,
|
||||
new HumanoidMarkingModifierState(component.MarkingSet, component.Species,
|
||||
component.Sex,
|
||||
component.SkinColor,
|
||||
component.CustomBaseLayers
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMarkingsSet(EntityUid uid, HumanoidAppearanceComponent component,
|
||||
HumanoidMarkingModifierMarkingSetMessage message)
|
||||
{
|
||||
if (!_adminManager.HasAdminFlag(message.Actor, AdminFlags.Fun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.MarkingSet = message.MarkingSet;
|
||||
Dirty(uid, component);
|
||||
|
||||
if (message.ResendState)
|
||||
{
|
||||
_uiSystem.SetUiState(
|
||||
uid,
|
||||
HumanoidMarkingModifierKey.Key,
|
||||
new HumanoidMarkingModifierState(component.MarkingSet, component.Species,
|
||||
component.Sex,
|
||||
component.SkinColor,
|
||||
component.CustomBaseLayers
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
|
||||
namespace Content.Server.Humanoid;
|
||||
|
||||
public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
{
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, HumanoidMarkingModifierMarkingSetMessage>(OnMarkingsSet);
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, HumanoidMarkingModifierBaseLayersSetMessage>(OnBaseLayersSet);
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, GetVerbsEvent<Verb>>(OnVerbsRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a marking from a humanoid by ID.
|
||||
/// </summary>
|
||||
/// <param name="uid">Humanoid mob's UID</param>
|
||||
/// <param name="marking">The marking to try and remove.</param>
|
||||
/// <param name="sync">Whether to immediately sync this to the humanoid</param>
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public void RemoveMarking(EntityUid uid, string marking, bool sync = true, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (!Resolve(uid, ref humanoid)
|
||||
|| !_markingManager.Markings.TryGetValue(marking, out var prototype))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
humanoid.MarkingSet.Remove(prototype.MarkingCategory, marking);
|
||||
|
||||
if (sync)
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a marking from a humanoid by category and index.
|
||||
/// </summary>
|
||||
/// <param name="uid">Humanoid mob's UID</param>
|
||||
/// <param name="category">Category of the marking</param>
|
||||
/// <param name="index">Index of the marking</param>
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public void RemoveMarking(EntityUid uid, MarkingCategories category, int index, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (index < 0
|
||||
|| !Resolve(uid, ref humanoid)
|
||||
|| !humanoid.MarkingSet.TryGetCategory(category, out var markings)
|
||||
|| index >= markings.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
humanoid.MarkingSet.Remove(category, index);
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the marking ID of the humanoid in a category at an index in the category's list.
|
||||
/// </summary>
|
||||
/// <param name="uid">Humanoid mob's UID</param>
|
||||
/// <param name="category">Category of the marking</param>
|
||||
/// <param name="index">Index of the marking</param>
|
||||
/// <param name="markingId">The marking ID to use</param>
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public void SetMarkingId(EntityUid uid, MarkingCategories category, int index, string markingId, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (index < 0
|
||||
|| !_markingManager.MarkingsByCategory(category).TryGetValue(markingId, out var markingPrototype)
|
||||
|| !Resolve(uid, ref humanoid)
|
||||
|| !humanoid.MarkingSet.TryGetCategory(category, out var markings)
|
||||
|| index >= markings.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var marking = markingPrototype.AsMarking();
|
||||
for (var i = 0; i < marking.MarkingColors.Count && i < markings[index].MarkingColors.Count; i++)
|
||||
{
|
||||
marking.SetColor(i, markings[index].MarkingColors[i]);
|
||||
}
|
||||
|
||||
humanoid.MarkingSet.Replace(category, index, marking);
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the marking colors of the humanoid in a category at an index in the category's list.
|
||||
/// </summary>
|
||||
/// <param name="uid">Humanoid mob's UID</param>
|
||||
/// <param name="category">Category of the marking</param>
|
||||
/// <param name="index">Index of the marking</param>
|
||||
/// <param name="colors">The marking colors to use</param>
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public void SetMarkingColor(EntityUid uid, MarkingCategories category, int index, List<Color> colors,
|
||||
HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (index < 0
|
||||
|| !Resolve(uid, ref humanoid)
|
||||
|| !humanoid.MarkingSet.TryGetCategory(category, out var markings)
|
||||
|| index >= markings.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < markings[index].MarkingColors.Count && i < colors.Count; i++)
|
||||
{
|
||||
markings[index].SetColor(i, colors[i]);
|
||||
}
|
||||
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.CharacterAppearance.Components;
|
||||
using Content.Server.Humanoid.Components;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Preferences;
|
||||
|
||||
@@ -6,8 +7,9 @@ namespace Content.Server.Humanoid.Systems;
|
||||
|
||||
public sealed class RandomHumanoidAppearanceSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -19,17 +21,13 @@ public sealed class RandomHumanoidAppearanceSystem : EntitySystem
|
||||
private void OnMapInit(EntityUid uid, RandomHumanoidAppearanceComponent component, MapInitEvent args)
|
||||
{
|
||||
// If we have an initial profile/base layer set, do not randomize this humanoid.
|
||||
if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid) || !string.IsNullOrEmpty(humanoid.Initial))
|
||||
{
|
||||
if (!TryComp<HumanoidProfileComponent>(uid, out var humanoid))
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
||||
//If we have a specified hair style, change it to this
|
||||
if(component.Hair != null)
|
||||
profile = profile.WithCharacterAppearance(profile.Appearance.WithHairStyleName(component.Hair));
|
||||
|
||||
_humanoid.LoadProfile(uid, profile, humanoid);
|
||||
_visualBody.ApplyProfileTo(uid, profile);
|
||||
_humanoidProfile.ApplyProfileTo(uid, profile);
|
||||
|
||||
if (component.RandomizeName)
|
||||
_metaData.SetEntityName(uid, profile.Name);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Content.Server.Humanoid.Components;
|
||||
using Content.Server.RandomMetadata;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -13,11 +15,11 @@ namespace Content.Server.Humanoid.Systems;
|
||||
/// </summary>
|
||||
public sealed class RandomHumanoidSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
@@ -44,8 +46,6 @@ public sealed class RandomHumanoidSystem : EntitySystem
|
||||
|
||||
_metaData.SetEntityName(humanoid, prototype.RandomizeName ? profile.Name : name);
|
||||
|
||||
_humanoid.LoadProfile(humanoid, profile);
|
||||
|
||||
if (prototype.Components != null)
|
||||
{
|
||||
foreach (var entry in prototype.Components.Values)
|
||||
@@ -58,6 +58,9 @@ public sealed class RandomHumanoidSystem : EntitySystem
|
||||
|
||||
EntityManager.InitializeAndStartEntity(humanoid);
|
||||
|
||||
_visualBody.ApplyProfileTo(humanoid, profile);
|
||||
_humanoidProfile.ApplyProfileTo(humanoid, profile);
|
||||
|
||||
return humanoid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.MagicMirror;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.MagicMirror;
|
||||
|
||||
/// <summary>
|
||||
/// Allows humanoids to change their appearance mid-round.
|
||||
/// </summary>
|
||||
public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly MarkingManager _markings = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
|
||||
private static readonly ProtoId<TagPrototype> HidesHairTag = "HidesHair";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Subs.BuiEvents<MagicMirrorComponent>(MagicMirrorUiKey.Key,
|
||||
subs =>
|
||||
{
|
||||
subs.Event<BoundUIClosedEvent>(OnUiClosed);
|
||||
subs.Event<MagicMirrorSelectMessage>(OnMagicMirrorSelect);
|
||||
subs.Event<MagicMirrorChangeColorMessage>(OnTryMagicMirrorChangeColor);
|
||||
subs.Event<MagicMirrorAddSlotMessage>(OnTryMagicMirrorAddSlot);
|
||||
subs.Event<MagicMirrorRemoveSlotMessage>(OnTryMagicMirrorRemoveSlot);
|
||||
});
|
||||
|
||||
|
||||
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorSelectDoAfterEvent>(OnSelectSlotDoAfter);
|
||||
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorChangeColorDoAfterEvent>(OnChangeColorDoAfter);
|
||||
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorRemoveSlotDoAfterEvent>(OnRemoveSlotDoAfter);
|
||||
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorAddSlotDoAfterEvent>(OnAddSlotDoAfter);
|
||||
}
|
||||
|
||||
private void OnMagicMirrorSelect(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectMessage message)
|
||||
{
|
||||
if (component.Target is not { } target)
|
||||
return;
|
||||
|
||||
// Check if the target getting their hair altered has any clothes that hides their hair
|
||||
if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
component.Target == message.Actor
|
||||
? Loc.GetString("magic-mirror-blocked-by-hat-self")
|
||||
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
|
||||
message.Actor,
|
||||
message.Actor,
|
||||
PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
_doAfterSystem.Cancel(component.DoAfter);
|
||||
component.DoAfter = null;
|
||||
|
||||
var doafterTime = component.SelectSlotTime;
|
||||
if (component.Target == message.Actor)
|
||||
doafterTime /= 3;
|
||||
|
||||
var doAfter = new MagicMirrorSelectDoAfterEvent()
|
||||
{
|
||||
Category = message.Category,
|
||||
Slot = message.Slot,
|
||||
Marking = message.Marking,
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid)
|
||||
{
|
||||
DistanceThreshold = SharedInteractionSystem.InteractionRange,
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
},
|
||||
out var doAfterId);
|
||||
|
||||
if (component.Target == message.Actor)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
|
||||
component.DoAfter = doAfterId;
|
||||
_audio.PlayPvs(component.ChangeHairSound, uid);
|
||||
}
|
||||
|
||||
private void OnSelectSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectDoAfterEvent args)
|
||||
{
|
||||
component.DoAfter = null;
|
||||
|
||||
if (args.Handled || args.Target == null || args.Cancelled)
|
||||
return;
|
||||
|
||||
if (component.Target != args.Target)
|
||||
return;
|
||||
|
||||
MarkingCategories category;
|
||||
|
||||
switch (args.Category)
|
||||
{
|
||||
case MagicMirrorCategory.Hair:
|
||||
category = MarkingCategories.Hair;
|
||||
break;
|
||||
case MagicMirrorCategory.FacialHair:
|
||||
category = MarkingCategories.FacialHair;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
_humanoid.SetMarkingId(component.Target.Value, category, args.Slot, args.Marking);
|
||||
|
||||
UpdateInterface(uid, component.Target.Value, component);
|
||||
}
|
||||
|
||||
private void OnTryMagicMirrorChangeColor(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorMessage message)
|
||||
{
|
||||
if (component.Target is not { } target)
|
||||
return;
|
||||
|
||||
// Check if the target getting their hair altered has any clothes that hides their hair
|
||||
if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
component.Target == message.Actor
|
||||
? Loc.GetString("magic-mirror-blocked-by-hat-self")
|
||||
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
|
||||
message.Actor,
|
||||
message.Actor,
|
||||
PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
_doAfterSystem.Cancel(component.DoAfter);
|
||||
component.DoAfter = null;
|
||||
|
||||
var doafterTime = component.ChangeSlotTime;
|
||||
if (component.Target == message.Actor)
|
||||
doafterTime /= 3;
|
||||
|
||||
var doAfter = new MagicMirrorChangeColorDoAfterEvent()
|
||||
{
|
||||
Category = message.Category,
|
||||
Slot = message.Slot,
|
||||
Colors = message.Colors,
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true
|
||||
},
|
||||
out var doAfterId);
|
||||
|
||||
if (component.Target == message.Actor)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-change-color-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-change-color-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
|
||||
component.DoAfter = doAfterId;
|
||||
}
|
||||
private void OnChangeColorDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorDoAfterEvent args)
|
||||
{
|
||||
component.DoAfter = null;
|
||||
|
||||
if (args.Handled || args.Target == null || args.Cancelled)
|
||||
return;
|
||||
|
||||
if (component.Target != args.Target)
|
||||
return;
|
||||
|
||||
MarkingCategories category;
|
||||
switch (args.Category)
|
||||
{
|
||||
case MagicMirrorCategory.Hair:
|
||||
category = MarkingCategories.Hair;
|
||||
break;
|
||||
case MagicMirrorCategory.FacialHair:
|
||||
category = MarkingCategories.FacialHair;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
_humanoid.SetMarkingColor(component.Target.Value, category, args.Slot, args.Colors);
|
||||
|
||||
// using this makes the UI feel like total ass
|
||||
// que
|
||||
// UpdateInterface(uid, component.Target, message.Session);
|
||||
}
|
||||
|
||||
private void OnTryMagicMirrorRemoveSlot(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotMessage message)
|
||||
{
|
||||
if (component.Target is not { } target)
|
||||
return;
|
||||
|
||||
// Check if the target getting their hair altered has any clothes that hides their hair
|
||||
if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
component.Target == message.Actor
|
||||
? Loc.GetString("magic-mirror-blocked-by-hat-self")
|
||||
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
|
||||
message.Actor,
|
||||
message.Actor,
|
||||
PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
_doAfterSystem.Cancel(component.DoAfter);
|
||||
component.DoAfter = null;
|
||||
|
||||
var doafterTime = component.RemoveSlotTime;
|
||||
if (component.Target == message.Actor)
|
||||
doafterTime /= 3;
|
||||
|
||||
var doAfter = new MagicMirrorRemoveSlotDoAfterEvent()
|
||||
{
|
||||
Category = message.Category,
|
||||
Slot = message.Slot,
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid)
|
||||
{
|
||||
DistanceThreshold = SharedInteractionSystem.InteractionRange,
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true
|
||||
},
|
||||
out var doAfterId);
|
||||
|
||||
if (component.Target == message.Actor)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-remove-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-remove-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
|
||||
component.DoAfter = doAfterId;
|
||||
_audio.PlayPvs(component.ChangeHairSound, uid);
|
||||
}
|
||||
|
||||
private void OnRemoveSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotDoAfterEvent args)
|
||||
{
|
||||
component.DoAfter = null;
|
||||
|
||||
if (args.Handled || args.Target == null || args.Cancelled)
|
||||
return;
|
||||
|
||||
if (component.Target != args.Target)
|
||||
return;
|
||||
|
||||
MarkingCategories category;
|
||||
|
||||
switch (args.Category)
|
||||
{
|
||||
case MagicMirrorCategory.Hair:
|
||||
category = MarkingCategories.Hair;
|
||||
break;
|
||||
case MagicMirrorCategory.FacialHair:
|
||||
category = MarkingCategories.FacialHair;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
_humanoid.RemoveMarking(component.Target.Value, category, args.Slot);
|
||||
|
||||
UpdateInterface(uid, component.Target.Value, component);
|
||||
}
|
||||
|
||||
private void OnTryMagicMirrorAddSlot(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotMessage message)
|
||||
{
|
||||
if (component.Target == null)
|
||||
return;
|
||||
|
||||
// Check if the target getting their hair altered has any clothes that hides their hair
|
||||
if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
component.Target == message.Actor
|
||||
? Loc.GetString("magic-mirror-blocked-by-hat-self")
|
||||
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
|
||||
message.Actor,
|
||||
message.Actor,
|
||||
PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
_doAfterSystem.Cancel(component.DoAfter);
|
||||
component.DoAfter = null;
|
||||
|
||||
var doafterTime = component.AddSlotTime;
|
||||
if (component.Target == message.Actor)
|
||||
doafterTime /= 3;
|
||||
|
||||
var doAfter = new MagicMirrorAddSlotDoAfterEvent()
|
||||
{
|
||||
Category = message.Category,
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: component.Target.Value, used: uid)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
},
|
||||
out var doAfterId);
|
||||
|
||||
if (component.Target == message.Actor)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-add-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("magic-mirror-add-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
|
||||
}
|
||||
|
||||
component.DoAfter = doAfterId;
|
||||
_audio.PlayPvs(component.ChangeHairSound, uid);
|
||||
}
|
||||
private void OnAddSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotDoAfterEvent args)
|
||||
{
|
||||
component.DoAfter = null;
|
||||
|
||||
if (args.Handled || args.Target == null || args.Cancelled || !TryComp(component.Target, out HumanoidAppearanceComponent? humanoid))
|
||||
return;
|
||||
|
||||
MarkingCategories category;
|
||||
|
||||
switch (args.Category)
|
||||
{
|
||||
case MagicMirrorCategory.Hair:
|
||||
category = MarkingCategories.Hair;
|
||||
break;
|
||||
case MagicMirrorCategory.FacialHair:
|
||||
category = MarkingCategories.FacialHair;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
var marking = _markings.MarkingsByCategoryAndSpecies(category, humanoid.Species).Keys.FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(marking))
|
||||
return;
|
||||
|
||||
_humanoid.AddMarking(component.Target.Value, marking, Color.Black);
|
||||
|
||||
UpdateInterface(uid, component.Target.Value, component);
|
||||
|
||||
}
|
||||
|
||||
private void OnUiClosed(Entity<MagicMirrorComponent> ent, ref BoundUIClosedEvent args)
|
||||
{
|
||||
ent.Comp.Target = null;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function that checks if the wearer has anything on their head
|
||||
/// Or if they have any clothes that hides their hair
|
||||
/// </summary>
|
||||
private bool CheckHeadSlotOrClothes(EntityUid user, EntityUid target)
|
||||
{
|
||||
if (TryComp<InventoryComponent>(target, out var inventoryComp))
|
||||
{
|
||||
// any hat whatsoever will block haircutting
|
||||
if (_inventory.TryGetSlotEntity(target, "head", out var hat, inventoryComp))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// maybe there's some kind of armor that has the HidesHair tag as well, so check every slot for it
|
||||
var slots = _inventory.GetSlotEnumerator((target, inventoryComp), SlotFlags.WITHOUT_POCKET);
|
||||
while (slots.MoveNext(out var slot))
|
||||
{
|
||||
if (slot.ContainedEntity != null && _tagSystem.HasTag(slot.ContainedEntity.Value, HidesHairTag))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
|
||||
|
||||
if (CanGib(uid, item, component))
|
||||
{
|
||||
var logImpact = HasComp<HumanoidAppearanceComponent>(item) ? LogImpact.Extreme : LogImpact.Medium;
|
||||
var logImpact = HasComp<HumanoidProfileComponent>(item) ? LogImpact.Extreme : LogImpact.Medium;
|
||||
_adminLogger.Add(LogType.Gib, logImpact, $"{ToPrettyString(item):victim} was gibbed by {ToPrettyString(uid):entity} ");
|
||||
if (component.ReclaimSolutions)
|
||||
SpawnChemicalsFromComposition(uid, item, completion, false, component, xform);
|
||||
|
||||
@@ -254,7 +254,7 @@ namespace Content.Server.Medical.BiomassReclaimer
|
||||
|
||||
// Reject souled bodies in easy mode.
|
||||
if (_configManager.GetCVar(CCVars.BiomassEasyMode) &&
|
||||
HasComp<HumanoidAppearanceComponent>(dragged) &&
|
||||
HasComp<HumanoidProfileComponent>(dragged) &&
|
||||
_minds.TryGetMind(dragged, out _, out var mind))
|
||||
{
|
||||
if (mind.UserId != null && _playerManager.TryGetSessionById(mind.UserId.Value, out _))
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class HijackShuttleConditionSystem : EntitySystem
|
||||
private bool IsShuttleHijacked(EntityUid shuttleGridId, EntityUid mindId)
|
||||
{
|
||||
var gridPlayers = Filter.BroadcastGrid(shuttleGridId).Recipients;
|
||||
var humanoids = GetEntityQuery<HumanoidAppearanceComponent>();
|
||||
var humanoids = GetEntityQuery<HumanoidProfileComponent>();
|
||||
var cuffable = GetEntityQuery<CuffableComponent>();
|
||||
EntityQuery<MobStateComponent>();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class SpeciesRequirementSystem : EntitySystem
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (!TryComp<HumanoidAppearanceComponent>(args.Mind.OwnedEntity, out var appearance)) {
|
||||
if (!TryComp<HumanoidProfileComponent>(args.Mind.OwnedEntity, out var appearance)) {
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Actions;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Buckle;
|
||||
using Content.Shared.Coordinates;
|
||||
using Content.Shared.Damage.Components;
|
||||
@@ -34,13 +35,13 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
[Dependency] private readonly SharedBuckleSystem _buckle = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
|
||||
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
|
||||
@@ -262,7 +263,7 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
|
||||
if (configuration.TransferHumanoidAppearance)
|
||||
{
|
||||
_humanoid.CloneAppearance(uid, child);
|
||||
_visualBody.CopyAppearanceFrom(uid, child);
|
||||
}
|
||||
|
||||
if (_mindSystem.TryGetMind(uid, out var mindId, out var mind))
|
||||
|
||||
@@ -76,7 +76,7 @@ public sealed partial class RevenantSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasComp<MobStateComponent>(target) || !HasComp<HumanoidAppearanceComponent>(target) || HasComp<RevenantComponent>(target))
|
||||
if (!HasComp<MobStateComponent>(target) || !HasComp<HumanoidProfileComponent>(target) || HasComp<RevenantComponent>(target))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed partial class SalvageSystem
|
||||
}
|
||||
|
||||
// TODO: This is terrible but need bluespace harnesses or something.
|
||||
var query = EntityQueryEnumerator<HumanoidAppearanceComponent, MobStateComponent, TransformComponent>();
|
||||
var query = EntityQueryEnumerator<HumanoidProfileComponent, MobStateComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _, out var mobState, out var mobXform))
|
||||
{
|
||||
|
||||
@@ -117,7 +117,7 @@ public sealed class VocalSystem : EntitySystem
|
||||
if (component.Sounds == null)
|
||||
return;
|
||||
|
||||
sex ??= CompOrNull<HumanoidAppearanceComponent>(uid)?.Sex ?? Sex.Unsexed;
|
||||
sex ??= CompOrNull<HumanoidProfileComponent>(uid)?.Sex ?? Sex.Unsexed;
|
||||
|
||||
if (!component.Sounds.TryGetValue(sex.Value, out var protoId))
|
||||
return;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Server.PDA;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.DetailExaminable;
|
||||
@@ -36,7 +37,8 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
|
||||
[Dependency] private readonly ActorSystem _actors = default!;
|
||||
[Dependency] private readonly IdCardSystem _cardSystem = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly PdaSystem _pdaSystem = default!;
|
||||
@@ -124,7 +126,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
|
||||
return jobEntity;
|
||||
}
|
||||
|
||||
string speciesId = profile != null ? profile.Species : SharedHumanoidAppearanceSystem.DefaultSpecies;
|
||||
string speciesId = profile != null ? profile.Species : HumanoidCharacterProfile.DefaultSpecies;
|
||||
|
||||
if (!_prototypeManager.TryIndex<SpeciesPrototype>(speciesId, out var species))
|
||||
throw new ArgumentException($"Invalid species prototype was used: {speciesId}");
|
||||
@@ -133,7 +135,8 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_humanoidSystem.LoadProfile(entity.Value, profile);
|
||||
_visualBody.ApplyProfileTo(entity.Value, profile);
|
||||
_humanoidProfile.ApplyProfileTo(entity.Value, profile);
|
||||
_metaSystem.SetEntityName(entity.Value, profile.Name);
|
||||
|
||||
if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText))
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class MassHallucinationsRule : StationEventSystem<MassHallucinatio
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
var query = EntityQueryEnumerator<MindContainerComponent, HumanoidAppearanceComponent>();
|
||||
var query = EntityQueryEnumerator<MindContainerComponent, HumanoidProfileComponent>();
|
||||
while (query.MoveNext(out var ent, out _, out _))
|
||||
{
|
||||
if (!EnsureComp<ParacusiaComponent>(ent, out var paracusia))
|
||||
|
||||
@@ -31,18 +31,18 @@ public sealed partial class BuyerSpeciesCondition : ListingCondition
|
||||
if (!ent.TryGetComponent<MindComponent>(args.Buyer, out var mind))
|
||||
return true; // needed to obtain body entityuid to check for humanoid appearance
|
||||
|
||||
if (!ent.TryGetComponent<HumanoidAppearanceComponent>(mind.OwnedEntity, out var appearance))
|
||||
if (!ent.TryGetComponent<HumanoidProfileComponent>(mind.OwnedEntity, out var humanoid))
|
||||
return true; // inanimate or non-humanoid entities should be handled elsewhere, main example being surplus crates
|
||||
|
||||
if (Blacklist != null)
|
||||
{
|
||||
if (Blacklist.Contains(appearance.Species))
|
||||
if (Blacklist.Contains(humanoid.Species))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Whitelist != null)
|
||||
{
|
||||
if (!Whitelist.Contains(appearance.Species))
|
||||
if (!Whitelist.Contains(humanoid.Species))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -11,19 +12,19 @@ namespace Content.Server.Cloning.Commands;
|
||||
[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
|
||||
public sealed class CloneCommand : ToolshedCommand
|
||||
{
|
||||
private HumanoidAppearanceSystem? _appearance;
|
||||
private SharedVisualBodySystem? _visualBody;
|
||||
private CloningSystem? _cloning;
|
||||
private MetaDataSystem? _metadata;
|
||||
|
||||
[CommandImplementation("humanoidappearance")]
|
||||
public IEnumerable<EntityUid> HumanoidAppearance([PipedArgument] IEnumerable<EntityUid> targets, EntityUid source, bool rename)
|
||||
{
|
||||
_appearance ??= GetSys<HumanoidAppearanceSystem>();
|
||||
_visualBody ??= GetSys<SharedVisualBodySystem>();
|
||||
_metadata ??= GetSys<MetaDataSystem>();
|
||||
|
||||
foreach (var ent in targets)
|
||||
{
|
||||
_appearance.CloneAppearance(source, ent);
|
||||
_visualBody.CopyAppearanceFrom(source, ent);
|
||||
|
||||
if (rename)
|
||||
_metadata.SetEntityName(ent, MetaData(source).EntityName, raiseEvents: true);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Wagging;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Wagging;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Content.Server.Wagging;
|
||||
public sealed class WaggingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -38,75 +38,91 @@ public sealed class WaggingSystem : EntitySystem
|
||||
EnsureComp<WaggingComponent>(args.CloneUid);
|
||||
}
|
||||
|
||||
private void OnWaggingMapInit(EntityUid uid, WaggingComponent component, MapInitEvent args)
|
||||
private void OnWaggingMapInit(Entity<WaggingComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
_actions.AddAction(uid, ref component.ActionEntity, component.Action, uid);
|
||||
_actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.Action, ent);
|
||||
}
|
||||
|
||||
private void OnWaggingShutdown(EntityUid uid, WaggingComponent component, ComponentShutdown args)
|
||||
private void OnWaggingShutdown(Entity<WaggingComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_actions.RemoveAction(uid, component.ActionEntity);
|
||||
_actions.RemoveAction(ent.Owner, ent.Comp.ActionEntity);
|
||||
}
|
||||
|
||||
private void OnWaggingToggle(EntityUid uid, WaggingComponent component, ref ToggleActionEvent args)
|
||||
private void OnWaggingToggle(Entity<WaggingComponent> ent, ref ToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
TryToggleWagging(uid, wagging: component);
|
||||
TryToggleWagging(ent.AsNullable());
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(EntityUid uid, WaggingComponent component, MobStateChangedEvent args)
|
||||
private void OnMobStateChanged(Entity<WaggingComponent> ent, ref MobStateChangedEvent args)
|
||||
{
|
||||
if (component.Wagging)
|
||||
TryToggleWagging(uid, wagging: component);
|
||||
if (ent.Comp.Wagging)
|
||||
TryToggleWagging(ent.AsNullable());
|
||||
}
|
||||
|
||||
public bool TryToggleWagging(EntityUid uid, WaggingComponent? wagging = null, HumanoidAppearanceComponent? humanoid = null)
|
||||
private bool TryToggleWagging(Entity<WaggingComponent?> ent)
|
||||
{
|
||||
if (!Resolve(uid, ref wagging, ref humanoid))
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
if (!humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Tail, out var markings))
|
||||
return false;
|
||||
|
||||
if (markings.Count == 0)
|
||||
return false;
|
||||
|
||||
wagging.Wagging = !wagging.Wagging;
|
||||
|
||||
for (var idx = 0; idx < markings.Count; idx++) // Animate all possible tails
|
||||
if (!_visualBody.TryGatherMarkingsData(ent.Owner,
|
||||
[ent.Comp.Layer],
|
||||
out _,
|
||||
out _,
|
||||
out var applied))
|
||||
{
|
||||
var currentMarkingId = markings[idx].MarkingId;
|
||||
string newMarkingId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wagging.Wagging)
|
||||
if (!applied.TryGetValue(ent.Comp.Organ, out var markingsSet))
|
||||
return false;
|
||||
|
||||
ent.Comp.Wagging = !ent.Comp.Wagging;
|
||||
|
||||
markingsSet = markingsSet.ShallowClone();
|
||||
foreach (var (layers, markings) in markingsSet)
|
||||
{
|
||||
markingsSet[layers] = markingsSet[layers].ShallowClone();
|
||||
var layerMarkings = markingsSet[layers];
|
||||
|
||||
for (int i = 0; i < layerMarkings.Count; i++)
|
||||
{
|
||||
newMarkingId = $"{currentMarkingId}{wagging.Suffix}";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentMarkingId.EndsWith(wagging.Suffix))
|
||||
var currentMarkingId = layerMarkings[i].MarkingId;
|
||||
string newMarkingId;
|
||||
|
||||
if (ent.Comp.Wagging)
|
||||
{
|
||||
newMarkingId = currentMarkingId[..^wagging.Suffix.Length];
|
||||
newMarkingId = $"{currentMarkingId}{ent.Comp.Suffix}";
|
||||
}
|
||||
else
|
||||
{
|
||||
newMarkingId = currentMarkingId;
|
||||
Log.Warning($"Unable to revert wagging for {currentMarkingId}");
|
||||
if (currentMarkingId.EndsWith(ent.Comp.Suffix))
|
||||
{
|
||||
newMarkingId = currentMarkingId[..^ent.Comp.Suffix.Length];
|
||||
}
|
||||
else
|
||||
{
|
||||
newMarkingId = currentMarkingId;
|
||||
Log.Warning($"Unable to revert wagging for {currentMarkingId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_prototype.HasIndex<MarkingPrototype>(newMarkingId))
|
||||
{
|
||||
Log.Warning($"{ToPrettyString(uid)} tried toggling wagging but {newMarkingId} marking doesn't exist");
|
||||
continue;
|
||||
}
|
||||
if (!_prototype.HasIndex<MarkingPrototype>(newMarkingId))
|
||||
{
|
||||
Log.Warning($"{ToPrettyString(ent):ent} tried toggling wagging but {newMarkingId} marking doesn't exist");
|
||||
continue;
|
||||
}
|
||||
|
||||
_humanoidAppearance.SetMarkingId(uid, MarkingCategories.Tail, idx, newMarkingId,
|
||||
humanoid: humanoid);
|
||||
layerMarkings[i] = new Marking(newMarkingId, layerMarkings[i].MarkingColors);
|
||||
}
|
||||
}
|
||||
|
||||
_visualBody.ApplyMarkings(ent, new()
|
||||
{
|
||||
[ent.Comp.Organ] = markingsSet
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class XAEPolymorphSystem : BaseXAESystem<XAEPolymorphComponent>
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
/// <summary> Pre-allocated and re-used collection.</summary>
|
||||
private readonly HashSet<Entity<HumanoidAppearanceComponent>> _humanoids = new();
|
||||
private readonly HashSet<Entity<HumanoidProfileComponent>> _humanoids = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnActivated(Entity<XAEPolymorphComponent> ent, ref XenoArtifactNodeActivatedEvent args)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
@@ -13,6 +14,7 @@ using Content.Server.NPC.HTN;
|
||||
using Content.Server.NPC.Systems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
@@ -35,6 +37,7 @@ using Content.Shared.Prying.Components;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Content.Shared.Ghost.Roles.Components;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Player;
|
||||
@@ -60,7 +63,7 @@ public sealed partial class ZombieSystem
|
||||
[Dependency] private readonly NpcFactionSystem _faction = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
@@ -75,6 +78,7 @@ public sealed partial class ZombieSystem
|
||||
private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
|
||||
private static readonly string MindRoleZombie = "MindRoleZombie";
|
||||
private static readonly List<ProtoId<AntagPrototype>> BannableZombiePrototypes = ["Zombie"];
|
||||
private static readonly HashSet<HumanoidVisualLayers> AdditionalZombieLayers = [HumanoidVisualLayers.Tail, HumanoidVisualLayers.HeadSide, HumanoidVisualLayers.HeadTop, HumanoidVisualLayers.Snout];
|
||||
|
||||
/// <summary>
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
@@ -186,27 +190,43 @@ public sealed partial class ZombieSystem
|
||||
_autoEmote.AddEmote(target, "ZombieGroan");
|
||||
}
|
||||
|
||||
//We have specific stuff for humanoid zombies because they matter more
|
||||
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
|
||||
if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReferenceSolution is { } reagents)
|
||||
zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
|
||||
|
||||
if (_visualBody.TryGatherMarkingsData(target, null, out var profiles, out _, out var markings))
|
||||
{
|
||||
//store some values before changing them in case the humanoid get cloned later
|
||||
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
|
||||
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
|
||||
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
|
||||
if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReferenceSolution is { } reagents)
|
||||
zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
|
||||
// TODO: My kingdom for ZombieSystem just using cloning system
|
||||
zombiecomp.BeforeZombifiedProfiles = profiles;
|
||||
zombiecomp.BeforeZombifiedMarkings = markings.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.ToDictionary(
|
||||
it => it.Key,
|
||||
it => it.Value.Select(marking => new Marking(marking)).ToList()));
|
||||
|
||||
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
|
||||
var zombifiedProfiles = profiles.ToDictionary(pair => pair.Key,
|
||||
pair => pair.Value with { EyeColor = zombiecomp.EyeColor, SkinColor = zombiecomp.SkinColor });
|
||||
_visualBody.ApplyProfiles(target, zombifiedProfiles);
|
||||
|
||||
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
|
||||
huApComp.EyeColor = zombiecomp.EyeColor;
|
||||
foreach (var markingSet in markings.Values)
|
||||
{
|
||||
foreach (var (layer, layerMarkings) in markingSet)
|
||||
{
|
||||
if (!AdditionalZombieLayers.Contains(layer))
|
||||
continue;
|
||||
|
||||
// this might not resync on clone?
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||
foreach (var marking in layerMarkings)
|
||||
{
|
||||
marking.SetColor(zombiecomp.SkinColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_visualBody.ApplyMarkings(target, markings);
|
||||
}
|
||||
|
||||
//We have specific stuff for humanoid zombies because they matter more
|
||||
if (HasComp<HumanoidProfileComponent>(target))
|
||||
{
|
||||
//This is done here because non-humanoids shouldn't get baller damage
|
||||
melee.Damage = zombiecomp.DamageOnBite;
|
||||
|
||||
|
||||
@@ -295,16 +295,9 @@ namespace Content.Server.Zombies
|
||||
if (!Resolve(source, ref zombiecomp))
|
||||
return false;
|
||||
|
||||
foreach (var (layer, info) in zombiecomp.BeforeZombifiedCustomBaseLayers)
|
||||
{
|
||||
_humanoidAppearance.SetBaseLayerColor(target, layer, info.Color);
|
||||
_humanoidAppearance.SetBaseLayerId(target, layer, info.Id);
|
||||
}
|
||||
if (TryComp<HumanoidAppearanceComponent>(target, out var appcomp))
|
||||
{
|
||||
appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor;
|
||||
}
|
||||
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
|
||||
_visualBody.ApplyProfiles(target, zombiecomp.BeforeZombifiedProfiles);
|
||||
_visualBody.ApplyMarkings(target, zombiecomp.BeforeZombifiedMarkings);
|
||||
|
||||
_bloodstream.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Medical;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
@@ -11,6 +12,10 @@ public sealed partial class BodySystem
|
||||
SubscribeLocalEvent<BodyComponent, ApplyMetabolicMultiplierEvent>(RefRelayBodyEvent);
|
||||
SubscribeLocalEvent<BodyComponent, TryVomitEvent>(RefRelayBodyEvent);
|
||||
SubscribeLocalEvent<BodyComponent, BeingGibbedEvent>(RefRelayBodyEvent);
|
||||
SubscribeLocalEvent<BodyComponent, ApplyOrganProfileDataEvent>(RefRelayBodyEvent);
|
||||
SubscribeLocalEvent<BodyComponent, ApplyOrganMarkingsEvent>(RefRelayBodyEvent);
|
||||
SubscribeLocalEvent<BodyComponent, OrganCopyAppearanceEvent>(RefRelayBodyEvent);
|
||||
SubscribeLocalEvent<BodyComponent, HumanoidLayerVisibilityChangedEvent>(RefRelayBodyEvent);
|
||||
}
|
||||
|
||||
private void RefRelayBodyEvent<T>(EntityUid uid, BodyComponent component, ref T args) where T : struct
|
||||
|
||||
14
Content.Shared/Body/InitialBodyComponent.cs
Normal file
14
Content.Shared/Body/InitialBodyComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
/// <summary>
|
||||
/// On map initialization, spawns the given organs into the body.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(InitialBodySystem))]
|
||||
public sealed partial class InitialBodyComponent : Component
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public Dictionary<ProtoId<OrganCategoryPrototype>, EntProtoId<OrganComponent>> Organs;
|
||||
}
|
||||
48
Content.Shared/Body/InitialBodySystem.cs
Normal file
48
Content.Shared/Body/InitialBodySystem.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
public sealed class InitialBodySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<InitialBodyComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<InitialBodyComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (!TryComp<ContainerManagerComponent>(ent, out var containerComp))
|
||||
return;
|
||||
|
||||
if (TerminatingOrDeleted(ent) || !Exists(ent))
|
||||
return;
|
||||
|
||||
if (!_container.TryGetContainer(ent, BodyComponent.ContainerID, out var container, containerComp))
|
||||
{
|
||||
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} is missing a container ({BodyComponent.ContainerID}).");
|
||||
return;
|
||||
}
|
||||
|
||||
var xform = Transform(ent);
|
||||
var coords = new EntityCoordinates(ent, Vector2.Zero);
|
||||
|
||||
foreach (var proto in ent.Comp.Organs.Values)
|
||||
{
|
||||
// TODO: When e#6192 is merged replace this all with TrySpawnInContainer...
|
||||
var spawn = Spawn(proto, coords);
|
||||
|
||||
if (!_container.Insert(spawn, container, containerXform: xform))
|
||||
{
|
||||
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} failed to insert an entity: {ToPrettyString(spawn)}.\n");
|
||||
Del(spawn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Content.Shared/Body/SharedVisualBodySystem.Initial.cs
Normal file
19
Content.Shared/Body/SharedVisualBodySystem.Initial.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Content.Shared.Humanoid;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
public abstract partial class SharedVisualBodySystem
|
||||
{
|
||||
private void InitializeInitial()
|
||||
{
|
||||
SubscribeLocalEvent<VisualBodyComponent, MapInitEvent>(OnVisualMapInit, after: [typeof(InitialBodySystem)]);
|
||||
}
|
||||
|
||||
private void OnVisualMapInit(Entity<VisualBodyComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (!TryComp<HumanoidProfileComponent>(ent, out var humanoidProfile))
|
||||
return;
|
||||
|
||||
ApplyAppearanceTo(ent.AsNullable(), HumanoidCharacterAppearance.DefaultWithSpecies(humanoidProfile.Species, humanoidProfile.Sex), humanoidProfile.Sex);
|
||||
}
|
||||
}
|
||||
153
Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs
Normal file
153
Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Administration.Managers;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
public abstract partial class SharedVisualBodySystem
|
||||
{
|
||||
[Dependency] private readonly ISharedAdminManager _admin = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
|
||||
|
||||
private void InitializeModifiers()
|
||||
{
|
||||
SubscribeLocalEvent<VisualBodyComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
|
||||
|
||||
Subs.BuiEvents<VisualBodyComponent>(HumanoidMarkingModifierKey.Key,
|
||||
subs =>
|
||||
{
|
||||
subs.Event<BoundUIOpenedEvent>(OnModifiersOpened);
|
||||
subs.Event<HumanoidMarkingModifierMarkingSetMessage>(OnSetModifiers);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnGetVerbs(Entity<VisualBodyComponent> ent, ref GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!_admin.HasAdminFlag(args.User, AdminFlags.Fun))
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
args.Verbs.Add(new Verb
|
||||
{
|
||||
Text = "Modify markings",
|
||||
Category = VerbCategory.Tricks,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"),
|
||||
Act = () =>
|
||||
{
|
||||
_userInterface.OpenUi(ent.Owner, HumanoidMarkingModifierKey.Key, user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers all the markings-relevant data from this entity
|
||||
/// </summary>
|
||||
/// <param name="ent">The entity to sample</param>
|
||||
/// <param name="filter">If set, only returns data concerning the given layers</param>
|
||||
/// <param name="profiles">The profiles for the various organs</param>
|
||||
/// <param name="markings">The marking parameters for the various organs</param>
|
||||
/// <param name="applied">The markings that are applied to the entity</param>
|
||||
public bool TryGatherMarkingsData(Entity<VisualBodyComponent?> ent,
|
||||
HashSet<HumanoidVisualLayers>? filter,
|
||||
[NotNullWhen(true)] out Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData>? profiles,
|
||||
[NotNullWhen(true)] out Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData>? markings,
|
||||
[NotNullWhen(true)] out Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>? applied)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
{
|
||||
profiles = null;
|
||||
markings = null;
|
||||
applied = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
profiles = new();
|
||||
markings = new();
|
||||
applied = new();
|
||||
|
||||
var organContainer = _container.EnsureContainer<Container>(ent, BodyComponent.ContainerID);
|
||||
|
||||
foreach (var organ in organContainer.ContainedEntities)
|
||||
{
|
||||
if (!TryComp<OrganComponent>(organ, out var organComp) || organComp.Category is not { } category)
|
||||
continue;
|
||||
|
||||
if (TryComp<VisualOrganComponent>(organ, out var visualOrgan))
|
||||
{
|
||||
profiles.TryAdd(category, visualOrgan.Profile);
|
||||
}
|
||||
|
||||
if (TryComp<VisualOrganMarkingsComponent>(organ, out var visualOrganMarkings))
|
||||
{
|
||||
markings.TryAdd(category, visualOrganMarkings.MarkingData);
|
||||
if (filter is not null)
|
||||
applied.TryAdd(category, visualOrganMarkings.Markings.Where(kvp => filter.Contains(kvp.Key)).ToDictionary());
|
||||
else
|
||||
applied.TryAdd(category, visualOrganMarkings.Markings);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnModifiersOpened(Entity<VisualBodyComponent> ent, ref BoundUIOpenedEvent args)
|
||||
{
|
||||
TryGatherMarkingsData(ent.AsNullable(), null, out var profiles, out var markings, out var applied);
|
||||
|
||||
_userInterface.SetUiState(ent.Owner, HumanoidMarkingModifierKey.Key, new HumanoidMarkingModifierState(applied!, markings!, profiles!));
|
||||
}
|
||||
|
||||
private void OnSetModifiers(Entity<VisualBodyComponent> ent, ref HumanoidMarkingModifierMarkingSetMessage args)
|
||||
{
|
||||
var markingsEvt = new ApplyOrganMarkingsEvent(args.Markings);
|
||||
RaiseLocalEvent(ent, ref markingsEvt);
|
||||
}
|
||||
|
||||
public void ApplyMarkings(EntityUid ent, Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> markings)
|
||||
{
|
||||
var markingsEvt = new ApplyOrganMarkingsEvent(markings);
|
||||
RaiseLocalEvent(ent, ref markingsEvt);
|
||||
}
|
||||
|
||||
private void ApplyAppearanceTo(Entity<VisualBodyComponent?> ent, HumanoidCharacterAppearance appearance, Sex sex)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
ApplyProfile(ent,
|
||||
new()
|
||||
{
|
||||
Sex = sex,
|
||||
SkinColor = appearance.SkinColor,
|
||||
EyeColor = appearance.EyeColor,
|
||||
});
|
||||
|
||||
var markingsEvt = new ApplyOrganMarkingsEvent(appearance.Markings);
|
||||
RaiseLocalEvent(ent, ref markingsEvt);
|
||||
}
|
||||
|
||||
public void ApplyProfileTo(Entity<VisualBodyComponent?> ent, HumanoidCharacterProfile profile)
|
||||
{
|
||||
ApplyAppearanceTo(ent, profile.Appearance, profile.Sex);
|
||||
}
|
||||
|
||||
public void ApplyProfile(EntityUid ent, OrganProfileData profile)
|
||||
{
|
||||
var profileEvt = new ApplyOrganProfileDataEvent(profile, null);
|
||||
RaiseLocalEvent(ent, ref profileEvt);
|
||||
}
|
||||
|
||||
public void ApplyProfiles(EntityUid ent, Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> profiles)
|
||||
{
|
||||
var profileEvt = new ApplyOrganProfileDataEvent(null, profiles);
|
||||
RaiseLocalEvent(ent, ref profileEvt);
|
||||
}
|
||||
}
|
||||
198
Content.Shared/Body/SharedVisualBodySystem.cs
Normal file
198
Content.Shared/Body/SharedVisualBodySystem.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
public abstract partial class SharedVisualBodySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly MarkingManager _marking = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<VisualOrganComponent, BodyRelayedEvent<OrganCopyAppearanceEvent>>(OnVisualOrganCopyAppearance);
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<OrganCopyAppearanceEvent>>(OnMarkingsOrganCopyAppearance);
|
||||
SubscribeLocalEvent<VisualOrganComponent, BodyRelayedEvent<ApplyOrganProfileDataEvent>>(OnVisualOrganApplyProfile);
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<ApplyOrganMarkingsEvent>>(OnMarkingsOrganApplyMarkings);
|
||||
|
||||
InitializeModifiers();
|
||||
InitializeInitial();
|
||||
}
|
||||
|
||||
private List<Marking> ResolveMarkings(List<Marking> markings, Color? skinColor, Color? eyeColor, Dictionary<Enum, MarkingsAppearance> appearances)
|
||||
{
|
||||
var ret = new List<Marking>();
|
||||
var forcedColors = new List<(Marking, MarkingPrototype)>();
|
||||
|
||||
// This method uses two loops since some marking with constrained colors care about the colors of previous markings.
|
||||
// As such we want to ensure we can apply the markings they rely on first.
|
||||
foreach (var marking in markings)
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
if (!proto.ForcedColoring && appearances.GetValueOrDefault(proto.BodyPart)?.MatchSkin != true)
|
||||
ret.Add(marking);
|
||||
else
|
||||
forcedColors.Add((marking, proto));
|
||||
}
|
||||
|
||||
foreach (var (marking, prototype) in forcedColors)
|
||||
{
|
||||
var colors = MarkingColoring.GetMarkingLayerColors(
|
||||
prototype,
|
||||
skinColor,
|
||||
eyeColor,
|
||||
ret);
|
||||
|
||||
var markingWithColor = new Marking(marking.MarkingId, colors)
|
||||
{
|
||||
Forced = marking.Forced,
|
||||
};
|
||||
if (appearances.GetValueOrDefault(prototype.BodyPart) is { MatchSkin: true } appearance && skinColor is { } color)
|
||||
{
|
||||
markingWithColor.SetColor(color.WithAlpha(appearance.LayerAlpha));
|
||||
}
|
||||
ret.Add(markingWithColor);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected virtual void SetOrganColor(Entity<VisualOrganComponent> ent, Color color)
|
||||
{
|
||||
ent.Comp.Data.Color = color;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
protected virtual void SetOrganAppearance(Entity<VisualOrganComponent> ent, PrototypeLayerData data)
|
||||
{
|
||||
ent.Comp.Data = data;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
protected virtual void SetOrganMarkings(Entity<VisualOrganMarkingsComponent> ent, Dictionary<HumanoidVisualLayers, List<Marking>> markings)
|
||||
{
|
||||
ent.Comp.Markings = markings;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
public void CopyAppearanceFrom(Entity<BodyComponent?> source, Entity<BodyComponent?> target)
|
||||
{
|
||||
if (!Resolve(source, ref source.Comp) || !Resolve(target, ref target.Comp))
|
||||
return;
|
||||
|
||||
var sourceOrgans = _container.EnsureContainer<Container>(source, BodyComponent.ContainerID);
|
||||
|
||||
foreach (var sourceOrgan in sourceOrgans.ContainedEntities)
|
||||
{
|
||||
var evt = new OrganCopyAppearanceEvent(sourceOrgan);
|
||||
RaiseLocalEvent(target, ref evt);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVisualOrganCopyAppearance(Entity<VisualOrganComponent> ent, ref BodyRelayedEvent<OrganCopyAppearanceEvent> args)
|
||||
{
|
||||
if (!TryComp<VisualOrganComponent>(args.Args.Organ, out var other))
|
||||
return;
|
||||
|
||||
if (!other.Layer.Equals(ent.Comp.Layer))
|
||||
return;
|
||||
|
||||
SetOrganAppearance(ent, other.Data);
|
||||
}
|
||||
|
||||
private void OnMarkingsOrganCopyAppearance(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<OrganCopyAppearanceEvent> args)
|
||||
{
|
||||
if (!TryComp<VisualOrganMarkingsComponent>(args.Args.Organ, out var other))
|
||||
return;
|
||||
|
||||
if (!other.MarkingData.Layers.SetEquals(ent.Comp.MarkingData.Layers))
|
||||
return;
|
||||
|
||||
SetOrganMarkings(ent, other.Markings);
|
||||
}
|
||||
|
||||
private void OnVisualOrganApplyProfile(Entity<VisualOrganComponent> ent, ref BodyRelayedEvent<ApplyOrganProfileDataEvent> args)
|
||||
{
|
||||
if (Comp<OrganComponent>(ent).Category is not { } category)
|
||||
return;
|
||||
|
||||
var relevantData = args.Args.Base;
|
||||
if (args.Args.Profiles?.TryGetValue(category, out var profile) == true)
|
||||
relevantData = profile;
|
||||
|
||||
if (relevantData is not { } data)
|
||||
return;
|
||||
|
||||
ent.Comp.Profile = data;
|
||||
|
||||
if (ent.Comp.Layer.Equals(HumanoidVisualLayers.Eyes))
|
||||
SetOrganColor(ent, ent.Comp.Profile.EyeColor);
|
||||
else
|
||||
SetOrganColor(ent, ent.Comp.Profile.SkinColor);
|
||||
}
|
||||
|
||||
private void OnMarkingsOrganApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<ApplyOrganMarkingsEvent> args)
|
||||
{
|
||||
if (Comp<OrganComponent>(ent).Category is not { } category)
|
||||
return;
|
||||
|
||||
if (!args.Args.Markings.TryGetValue(category, out var markingSet))
|
||||
return;
|
||||
|
||||
var groupProto = _prototype.Index(ent.Comp.MarkingData.Group);
|
||||
var organMarkings = ent.Comp.Markings.ShallowClone();
|
||||
|
||||
foreach (var layer in ent.Comp.MarkingData.Layers)
|
||||
{
|
||||
if (!markingSet.TryGetValue(layer, out var markings))
|
||||
continue;
|
||||
|
||||
var okSet = new List<Marking>();
|
||||
|
||||
foreach (var marking in markings)
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out _))
|
||||
continue;
|
||||
|
||||
okSet.Add(marking);
|
||||
}
|
||||
|
||||
organMarkings[layer] = okSet;
|
||||
}
|
||||
|
||||
var profile = Comp<VisualOrganComponent>(ent).Profile;
|
||||
var resolved = organMarkings.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => ResolveMarkings(kvp.Value, profile.SkinColor, profile.EyeColor, groupProto.Appearances));
|
||||
|
||||
SetOrganMarkings(ent, resolved);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on body entity, when an organ is having its appearance copied to it
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct OrganCopyAppearanceEvent(EntityUid Organ);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on body entity when profiles are being applied to it
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct ApplyOrganProfileDataEvent(OrganProfileData? Base, Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData>? Profiles);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on body entity when a profile is being applied to it
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct ApplyOrganMarkingsEvent(Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings);
|
||||
|
||||
7
Content.Shared/Body/VisualBodyComponent.cs
Normal file
7
Content.Shared/Body/VisualBodyComponent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedVisualBodySystem))]
|
||||
public sealed partial class VisualBodyComponent : Component;
|
||||
52
Content.Shared/Body/VisualOrganComponent.cs
Normal file
52
Content.Shared/Body/VisualOrganComponent.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
[Access(typeof(SharedVisualBodySystem))]
|
||||
public sealed partial class VisualOrganComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The layer on the entity that this contributes to
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public Enum Layer;
|
||||
|
||||
/// <summary>
|
||||
/// The data for the layer
|
||||
/// </summary>
|
||||
[DataField(required: true), AutoNetworkedField, AlwaysPushInheritance]
|
||||
public PrototypeLayerData Data;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public OrganProfileData Profile = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the coloration, sex, etc. of organs
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public partial record struct OrganProfileData
|
||||
{
|
||||
/// <summary>
|
||||
/// The "sex" of this organ
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Sex Sex;
|
||||
|
||||
/// <summary>
|
||||
/// The "eye color" of this organ
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color EyeColor = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// The "skin color" of this organ
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color SkinColor = Color.White;
|
||||
}
|
||||
|
||||
47
Content.Shared/Body/VisualOrganMarkingsComponent.cs
Normal file
47
Content.Shared/Body/VisualOrganMarkingsComponent.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Body;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)]
|
||||
[Access(typeof(SharedVisualBodySystem))]
|
||||
public sealed partial class VisualOrganMarkingsComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// What markings this organ can take
|
||||
/// </summary>
|
||||
[DataField(required: true), AlwaysPushInheritance]
|
||||
public OrganMarkingData MarkingData = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The list of markings to apply to the entity
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public Dictionary<HumanoidVisualLayers, List<Marking>> Markings = new();
|
||||
|
||||
/// <summary>
|
||||
/// Client only - the last markings applied by this component
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public List<Marking> AppliedMarkings = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the layers and group an organ takes markings for
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public partial record struct OrganMarkingData
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public HashSet<HumanoidVisualLayers> Layers = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The type of organ this is for markings
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<MarkingsGroupPrototype> Group = default!;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public sealed partial class ChangelingDevourComponent : Component
|
||||
Components =
|
||||
[
|
||||
"MobState",
|
||||
"HumanoidAppearance",
|
||||
"HumanoidProfile",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Database;
|
||||
@@ -19,7 +20,6 @@ namespace Content.Shared.Changeling.Systems;
|
||||
public sealed class ChangelingClonerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
@@ -29,6 +29,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!;
|
||||
[Dependency] private readonly SharedForensicsSystem _forensics = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -132,7 +133,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
|
||||
if (ent.Comp.State != ChangelingClonerState.Empty)
|
||||
return false;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
if (!HasComp<HumanoidProfileComponent>(target))
|
||||
return false; // cloning only works for humanoids at the moment
|
||||
|
||||
var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerDrawDoAfterEvent(), ent, target: target, used: ent)
|
||||
@@ -168,7 +169,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
|
||||
if (ent.Comp.State != ChangelingClonerState.Filled)
|
||||
return false;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
if (!HasComp<HumanoidProfileComponent>(target))
|
||||
return false; // cloning only works for humanoids at the moment
|
||||
|
||||
var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerInjectDoAfterEvent(), ent, target: target, used: ent)
|
||||
@@ -205,7 +206,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
|
||||
if (ent.Comp.State != ChangelingClonerState.Empty)
|
||||
return;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
if (!HasComp<HumanoidProfileComponent>(target))
|
||||
return; // cloning only works for humanoids at the moment
|
||||
|
||||
if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
|
||||
@@ -235,7 +236,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
|
||||
if (ent.Comp.State != ChangelingClonerState.Filled)
|
||||
return;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
if (!HasComp<HumanoidProfileComponent>(target))
|
||||
return; // cloning only works for humanoids at the moment
|
||||
|
||||
if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
|
||||
@@ -258,7 +259,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
|
||||
$"{user} is using {ent.Owner} to inject DNA into {target} changing their identity to {ent.Comp.ClonedBackup.Value}.");
|
||||
|
||||
// Do the actual transformation.
|
||||
_humanoidAppearance.CloneAppearance(ent.Comp.ClonedBackup.Value, target);
|
||||
_visualBody.CopyAppearanceFrom(ent.Comp.ClonedBackup.Value, target);
|
||||
_cloning.CloneComponents(ent.Comp.ClonedBackup.Value, target, settings);
|
||||
_metaData.SetEntityName(target, Name(ent.Comp.ClonedBackup.Value), raiseEvents: ent.Comp.RaiseNameChangeEvents);
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
||||
|
||||
if (_mobState.IsDead(target.Value)
|
||||
&& TryComp<BodyComponent>(target, out var body)
|
||||
&& HasComp<HumanoidAppearanceComponent>(target)
|
||||
&& HasComp<HumanoidProfileComponent>(target)
|
||||
&& TryComp<ChangelingIdentityComponent>(args.User, out var identityStorage))
|
||||
{
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(args.Target):player}'s identity");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Database;
|
||||
@@ -19,12 +20,12 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearanceSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedCloningSystem _cloningSystem = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface";
|
||||
@@ -152,7 +153,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
if (args.Target is not { } targetIdentity)
|
||||
return;
|
||||
|
||||
_humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User);
|
||||
_visualBody.CopyAppearanceFrom(targetIdentity, args.User);
|
||||
_cloningSystem.CloneComponents(targetIdentity, args.User, settings);
|
||||
|
||||
if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Humanoid;
|
||||
@@ -18,8 +19,8 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly SharedCloningSystem _cloningSystem = default!;
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly SharedMapSystem _map = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
[Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!;
|
||||
|
||||
public MapId? PausedMapId;
|
||||
@@ -93,7 +94,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
|
||||
if (_net.IsClient)
|
||||
return null;
|
||||
|
||||
if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid)
|
||||
if (!TryComp<HumanoidProfileComponent>(target, out var humanoid)
|
||||
|| !_prototype.Resolve(humanoid.Species, out var speciesPrototype))
|
||||
return null;
|
||||
|
||||
@@ -106,7 +107,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
|
||||
if (TryComp<ActorComponent>(target, out var actor))
|
||||
storedIdentity.OriginalSession = actor.PlayerSession;
|
||||
|
||||
_humanoidSystem.CloneAppearance(target, clone);
|
||||
_visualBody.CopyAppearanceFrom(target, clone);
|
||||
_cloningSystem.CloneComponents(target, clone, settings);
|
||||
|
||||
var targetName = _nameMod.GetBaseName(target);
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Content.Shared.Clothing.EntitySystems;
|
||||
|
||||
public sealed class HideLayerClothingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly SharedHideableHumanoidLayersSystem _hideableHumanoidLayers = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -36,7 +36,7 @@ public sealed class HideLayerClothingSystem : EntitySystem
|
||||
|
||||
private void SetLayerVisibility(
|
||||
Entity<HideLayerClothingComponent?, ClothingComponent?> clothing,
|
||||
Entity<HumanoidAppearanceComponent?> user,
|
||||
Entity<HideableHumanoidLayersComponent?> user,
|
||||
bool hideLayers)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
@@ -60,8 +60,6 @@ public sealed class HideLayerClothingSystem : EntitySystem
|
||||
DebugTools.AssertNotNull(clothing.Comp2.InSlotFlag);
|
||||
DebugTools.AssertNotEqual(inSlot, SlotFlags.NONE);
|
||||
|
||||
var dirty = false;
|
||||
|
||||
// iterate the HideLayerClothingComponent's layers map and check that
|
||||
// the clothing is (or was)equipped in a matching slot.
|
||||
foreach (var (layer, validSlots) in clothing.Comp1.Layers)
|
||||
@@ -71,7 +69,7 @@ public sealed class HideLayerClothingSystem : EntitySystem
|
||||
|
||||
// Only update this layer if we are currently equipped to the relevant slot.
|
||||
if (validSlots.HasFlag(inSlot))
|
||||
_humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
|
||||
_hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot);
|
||||
}
|
||||
|
||||
// Fallback for obsolete field: assume we want to hide **all** layers, as long as we are equipped to any
|
||||
@@ -83,12 +81,9 @@ public sealed class HideLayerClothingSystem : EntitySystem
|
||||
foreach (var layer in slots)
|
||||
{
|
||||
if (hideable.Contains(layer))
|
||||
_humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
|
||||
_hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(user!);
|
||||
}
|
||||
|
||||
private bool IsEnabled(Entity<HideLayerClothingComponent, ClothingComponent> clothing)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Preferences;
|
||||
@@ -28,7 +29,7 @@ public sealed class LoadoutSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
// Wait until the character has all their organs before we give them their loadout
|
||||
SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit, after: [typeof(InitialBodySystem)]);
|
||||
}
|
||||
|
||||
public static string GetJobPrototype(string? loadout)
|
||||
@@ -176,11 +177,8 @@ public sealed class LoadoutSystem : EntitySystem
|
||||
|
||||
public HumanoidCharacterProfile GetProfile(EntityUid? uid)
|
||||
{
|
||||
if (TryComp(uid, out HumanoidAppearanceComponent? appearance))
|
||||
{
|
||||
return HumanoidCharacterProfile.DefaultWithSpecies(appearance.Species);
|
||||
}
|
||||
|
||||
return HumanoidCharacterProfile.Random();
|
||||
return TryComp<HumanoidProfileComponent>(uid, out var profile)
|
||||
? HumanoidCharacterProfile.DefaultWithSpecies(profile.Species, profile.Sex)
|
||||
: HumanoidCharacterProfile.Random();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user