Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
224 changes: 213 additions & 11 deletions Content.Client/Humanoid/HumanoidAppearanceSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Content.Client.DisplacementMap;
using Content.Shared._Coyote.GenitalsShared;
using Content.Shared.CCVar;
using System.Numerics;
using Content.Shared.Humanoid;
Expand All @@ -17,6 +18,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
[Dependency] private readonly GenitalManager _genitalManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly DisplacementMapSystem _displacement = default!;

Expand Down Expand Up @@ -47,23 +49,201 @@ private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent
{
UpdateLayers(component, sprite);
ApplyMarkingSet(component, sprite);
// TODO: make this thing a more versatulate proc
var speciesPrototype = _prototypeManager.Index(component.Species);

var height = Math.Clamp(component.Height, speciesPrototype.MinHeight, speciesPrototype.MaxHeight);
var width = Math.Clamp(component.Width, speciesPrototype.MinWidth, speciesPrototype.MaxWidth);
component.Height = height;
component.Width = width;

sprite.Scale = new Vector2(width, height);
ResizeRescale(component, sprite);
UpdateLayersAgain(component, sprite); // cool
UpdateGenitals(component, sprite);

sprite[sprite.LayerMapReserveBlank(HumanoidVisualLayers.Eyes)].Color = component.EyeColor;
}

private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
=> humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);

/// <summary>
/// The master weenie handler.
/// Decrypts the genital data on the component and turns them into countless sprites.
/// </summary>
/// <param name="component"></param>
/// <param name="sprite"></param>
private void UpdateGenitals(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
// first, clear all the genitals
foreach (var gsm in component.GenitalSpriteMetaDatas)
{
if (!sprite.LayerMapTryGet(gsm.Key, out var index))
continue;
sprite.RemoveLayer(index);
sprite.LayerMapRemove(gsm.Key);
}
component.GenitalSpriteMetaDatas.Clear();

// okay first, make sure the genital sublayers for the various positions are present
EnsureGenitalLayers(component, sprite);

// if there are no genitals, we can stop here
if (component.Genitals.Count == 0)
return;
// now go through all the genitals and process them into their many pieces
foreach (var genital in component.Genitals)
{
_genitalManager.GetGenital(genital.Prototype, out var genShapeProt);
if (genShapeProt == null)
{
Logger.Warning($"Tried to apply genital {genital.Prototype} but it does not exist!");
continue;
}

_genitalManager.GetGenitalSize(
genital.Prototype,
ref genital.Size,
out var genSizeProt);
if (genSizeProt is null)
{
Logger.Warning($"Tried to apply genital size {genital.Size} for {genital.Prototype} but it does not exist!");
continue;
}
genital.Sprites.Clear(); // clear the genital sprites, we will be adding them again
// sprites in sprites, go through the GenitalSizeSpriteData list and make em
var sprites2Use = genital.Aroused
? genSizeProt.ArousedSprites ?? genSizeProt.Sprites
: genSizeProt.Sprites;
foreach (var spriteData in sprites2Use)
{
// pass the sprite data off to the genital sprite handler constructormat
// sorry, gotta be an RSI
ApplyGenitalSprite(
component, // the humanoid component
sprite, // the sprite component
genital, // the genital data
genShapeProt, // the genital shape prototype
spriteData, // the genital size sprite data
out var spriteMetadata,
out var errorRsiState);
if (!string.IsNullOrEmpty(errorRsiState)
|| spriteMetadata == null)
{
// if the sprite data is not valid, we log a warning and continue
Logger.Warning($"Failed to apply genital sprite for {genital.Prototype} size {genital.Size}, state {errorRsiState}!");
continue;
}
// add the data to the genital sprite keys
component.GenitalSpriteMetaDatas.Add(spriteMetadata);
}
}
}

private void EnsureGenitalLayers(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
{
// list of GenitalLayerGroups
var genitalGroups = _genitalManager.GetGenitalGroupStrings();
// list of every kind of genital in the game
var genitalSlots = _genitalManager.GetGenitalSlotStrings();
foreach (var geniGroup in genitalGroups)
{
if (!sprite.LayerMapTryGet(geniGroup, out var index))
{
continue; // they dont have it :c
}

foreach (var genitalSlot in genitalSlots)
{
// make sure the genital slot is not already present
var layerId = _genitalManager.GenitalLayerId(
geniGroup,
genitalSlot);
if (!sprite.LayerMapTryGet(layerId, out _))
{
// add it to the sprite component
var layerIndex = sprite.AddBlankLayer(index + 1); // put it under the group layer
}
}
}
}

/// <summary>
/// Applies a single genital sprite to the humanoid's sprite component.
/// </summary>
/// <param name="humanoid"></param>
/// <param name="sprite"></param>
/// <param name="genital"></param>
/// <param name="genitalShape"></param>
/// <param name="genitalSprite"></param>
/// <param name="layerId"></param>
/// <param name="spriteMetadata"></param>
/// <param name="errorRsiState"></param>
private void ApplyGenitalSprite(
HumanoidAppearanceComponent humanoid,
SpriteComponent sprite,
GenitalData genital,
GenitalShapePrototype genitalShape,
GenitalSizeSpriteData genitalSprite,
out GenitalSpriteMetaData? spriteMetadata,
out string? errorRsiState)
{
// first the pre-checks
// is it an RSI?
if (genitalSprite.Sprite is not SpriteSpecifier.Rsi rsi)
{
Logger.Warning($"Tried to apply genital {genital.Prototype} but it is not an RSI!");
spriteMetadata = null!;
errorRsiState = "NOT AN RSI";
return;
}

Color color = Color.White;
// is genitalSprite.ColorIndex a valid index?
if (genitalSprite.ColorIndex >= 0
&& genitalShape.ColorCount > genitalSprite.ColorIndex)
{
color = genital.Colors[genitalSprite.ColorIndex];
}
// if GenitalSprite.LayerGroup is set to VariableLayer, check genitalData for which slot to apply it to
var layerGroup = "";
if (genitalSprite.LayerGroup == GenitalSpritePositioning.VariableLayer)
{
// get the genital slot from the genital data
layerGroup = genital.LayeringMode.ToString();
}
else
{
layerGroup = genitalSprite.LayerGroup.ToString();
}
var layerSlot = _genitalManager.GenitalLayerId(
layerGroup,
genitalShape.Kind.ToString());
var visible = !genital.Hidden;
var layerId = _genitalManager.GenitalSpriteLayerId(
layerGroup,
genitalShape.Kind.ToString(),
rsi.RsiPath.Filename,
rsi.RsiState);
// okay now we know where to stick the sprite, lets do it
if (!sprite.LayerMapTryGet(layerSlot, out var targetLayer))
{
var targLayerAdj = targetLayer + 1;
var layer = sprite.AddLayer(genitalSprite.Sprite, targLayerAdj);
sprite.LayerMapSet(layerId, layer);
sprite.LayerSetSprite(layerId, rsi);
}
sprite.LayerSetVisible(layerId, visible);
sprite.LayerSetColor(layerId, color);
// spriteRsiPath
// spriteState
// colorIndex
// colorTrue
// layerGroup
// key
spriteMetadata = new GenitalSpriteMetaData(
rsi.RsiPath.Filename,
rsi.RsiState,
genitalSprite.ColorIndex,
color,
genitalSprite.LayerGroup,
layerId);
errorRsiState = string.Empty;
}


private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
var oldLayers = new HashSet<HumanoidVisualLayers>(component.BaseLayers.Keys);
Expand Down Expand Up @@ -187,6 +367,21 @@ public void HideBaseLayers(
}
}

/// <summary>
/// Resizes and rescales the sprite layers based on the humanoid's height and width.
/// </summary>
private void ResizeRescale(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
var speciesPrototype = _prototypeManager.Index(component.Species);

var height = Math.Clamp(component.Height, speciesPrototype.MinHeight, speciesPrototype.MaxHeight);
var width = Math.Clamp(component.Width, speciesPrototype.MinWidth, speciesPrototype.MaxWidth);
component.Height = height;
component.Width = width;

sprite.Scale = new Vector2(width, height);
}

/// <summary>
/// Loads a profile directly into a humanoid.
/// </summary>
Expand All @@ -197,7 +392,9 @@ public void HideBaseLayers(
/// 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)
public override void LoadProfile(EntityUid uid,
HumanoidCharacterProfile? profile,
HumanoidAppearanceComponent? humanoid = null)
{
if (profile == null)
return;
Expand Down Expand Up @@ -301,6 +498,7 @@ public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profil
humanoid.EyeColor = profile.Appearance.EyeColor;
humanoid.Height = profile.Height;
humanoid.Width = profile.Width;
humanoid.Genitals = profile.Appearance.Genitals;

UpdateSprite(humanoid, Comp<SpriteComponent>(uid));
}
Expand Down Expand Up @@ -332,6 +530,8 @@ private void ApplyMarkingSet(HumanoidAppearanceComponent humanoid, SpriteCompone
}
}

// TODO: Add get genitals event here

humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);

// AddUndergarments(humanoid, sprite, applyUndergarmentTop, applyUndergarmentBottom);
Expand Down Expand Up @@ -406,7 +606,9 @@ private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteCompon
}
}
}

#region Marking Application
// hi
#endregion
private void ApplyMarking(MarkingPrototype markingPrototype,
IReadOnlyList<Color>? colors,
bool visible,
Expand Down
1 change: 1 addition & 0 deletions Content.Server.Database/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ public class Profile
public string Species { get; set; } = null!;
public string Customspeciesname { get; set; } = null!;
[Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
[Column(TypeName = "jsonb")] public JsonDocument? Genitals { get; set; } = null!;
public string HairName { get; set; } = null!;
public string HairColor { get; set; } = null!;
public string FacialHairName { get; set; } = null!;
Expand Down
29 changes: 27 additions & 2 deletions Content.Server/Database/ServerDbBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,28 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
{
var parsed = Marking.ParseFromDbString(marking);

if (parsed is null) continue;
if (parsed is null)
continue;

markings.Add(parsed);
}
}

// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
var genitalsRaw = profile.Genitals?.Deserialize<List<string>>();
List<GenitalData> genitalia = new();
if (genitalsRaw != null)
{
foreach (var genital in genitalsRaw)
{
var parsed = GenitalData.ParseFromDbString(genital);
if (parsed is null)
continue;

genitalia.Add(parsed);
}
}

var loadouts = new Dictionary<string, RoleLoadout>();

foreach (var role in profile.Loadouts)
Expand Down Expand Up @@ -263,7 +279,8 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
Color.FromHex(profile.FacialHairColor),
Color.FromHex(profile.EyeColor),
Color.FromHex(profile.SkinColor),
markings
markings,
genitalia
),
spawnPriority,
jobs,
Expand All @@ -284,6 +301,13 @@ private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int sl
markingStrings.Add(marking.ToString());
}
var markings = JsonSerializer.SerializeToDocument(markingStrings);
// genitals
List<string> genitalStrings = new(); // ew genital strings
foreach (var genital in appearance.Genitals)
{
genitalStrings.Add(genital.Genital2String());
}
var genitals = JsonSerializer.SerializeToDocument(genitalStrings);

profile.CharacterName = humanoid.Name;
profile.FlavorText = humanoid.FlavorText;
Expand All @@ -303,6 +327,7 @@ private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int sl
profile.SkinColor = appearance.SkinColor.ToHex();
profile.SpawnPriority = (int) humanoid.SpawnPriority;
profile.Markings = markings;
profile.Genitals = genitals;
profile.Slot = slot;
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;

Expand Down
16 changes: 16 additions & 0 deletions Content.Server/_Coyote/Genitals/GenitalMisc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

namespace Content.Server._Coyote.Genitals;

/*
* File: GenitalMisc.cs
* Project: Content.Server._Coyote.Genitals
* Created Date: 2023-10-01
* License: What I finds, I keeps
* Description:
* This file contains various data structures and enums related to genitals.
* I dont like having multiple files for such a small amount of code,
* so I just shoved it all in here, like a good genital should.
*/

6 changes: 6 additions & 0 deletions Content.Server/_Coyote/Genitals/GenitalShapePrototype.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

namespace Content.Server._Coyote.Genitals;


13 changes: 13 additions & 0 deletions Content.Server/_Coyote/Genitals/GenitalsComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Content.Server._Coyote.Genitals;

/// <summary>
/// A handy dandy data manager for genitals! It'll keep track of all the genitals
/// in use, and then hijack the mob's appearance to insert them into the mob's
/// marking system thing. this is a brilliant idea and I love it.
/// </summary>
[RegisterComponent]
public sealed partial class GenitalsComponent : Component
{
}


Loading
Loading