CatStation/Content.Shared/Preferences/HumanoidCharacterProfile.cs
2026-09-20 03:32:36 +07:00

1058 lines
38 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
using Content.Shared.Corvax.TTS;
using Content.Shared.Chat.Prototypes;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Speech.Components;
using Content.Shared.Traits;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared;
using YamlDotNet.RepresentationModel;
using Content.Corvax.Interfaces.Shared; // Corvax-Sponsors
namespace Content.Shared.Preferences
{
/// <summary>
/// Character profile. Looks immutable, but uses non-immutable semantics internally for serialization/code sanity purposes.
/// </summary>
[DataDefinition]
[Serializable, NetSerializable]
public sealed partial class HumanoidCharacterProfile
{
public static readonly ProtoId<SpeciesPrototype> DefaultSpecies = "Human";
public static readonly ProtoId<EmoteSoundsPrototype> DefaultVoice = "MaleHuman";
private static readonly Regex RestrictedNameRegex = new("[^А-Яа-яёЁ0-9' -]"); // Corvax-Localization
private static readonly Regex ICNameCaseRegex = new(@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)");
/// <summary>
/// Job preferences for initial spawn.
/// </summary>
[DataField]
private Dictionary<ProtoId<JobPrototype>, JobPriority> _jobPriorities = new()
{
{
GameTicker.FallbackOverflowJob, JobPriority.High
}
};
/// <summary>
/// Antags we have opted in to.
/// </summary>
[DataField]
private HashSet<ProtoId<AntagPrototype>> _antagPreferences = new();
/// <summary>
/// Enabled traits.
/// </summary>
[DataField]
private HashSet<ProtoId<TraitPrototype>> _traitPreferences = new();
/// <summary>
/// <see cref="_loadouts"/>
/// </summary>
public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
[DataField]
private Dictionary<string, RoleLoadout> _loadouts = new();
[DataField]
public string Name { get; set; } = "John Doe";
/// <summary>
/// Detailed text that can appear for the character if <see cref="CCVars.FlavorText"/> is enabled.
/// </summary>
[DataField]
public string FlavorText { get; set; } = string.Empty;
/// <summary>
/// Associated <see cref="SpeciesPrototype"/> for this profile.
/// </summary>
[DataField]
public ProtoId<SpeciesPrototype> Species { get; set; } = DefaultSpecies;
[DataField] //Corvax-TTS
public string TTSVoice { get; set; } = HumanoidProfileSystem.DefaultVoice;
[DataField]
public int Age { get; set; } = 18;
[DataField]
public Sex Sex { get; private set; } = Sex.Male;
[DataField]
public ProtoId<EmoteSoundsPrototype> Voice { get; set; } = DefaultVoice;
[DataField]
public Gender Gender { get; private set; } = Gender.Male;
/// <summary>
/// Stores markings, eye colors, etc for the profile.
/// </summary>
[DataField]
public HumanoidCharacterAppearance Appearance { get; set; } = new();
/// <summary>
/// When spawning into a round what's the preferred spot to spawn.
/// </summary>
[DataField]
public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None;
/// <summary>
/// <see cref="_jobPriorities"/>
/// </summary>
public IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> JobPriorities => _jobPriorities;
/// <summary>
/// <see cref="_antagPreferences"/>
/// </summary>
public IReadOnlySet<ProtoId<AntagPrototype>> AntagPreferences => _antagPreferences;
/// <summary>
/// <see cref="_traitPreferences"/>
/// </summary>
public IReadOnlySet<ProtoId<TraitPrototype>> TraitPreferences => _traitPreferences;
/// <summary>
/// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
/// </summary>
[DataField]
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
PreferenceUnavailableMode.SpawnAsOverflow;
public HumanoidCharacterProfile(
string name,
string flavortext,
string species,
string TTS_voice, // Corvax-TTS
int age,
Sex sex,
ProtoId<EmoteSoundsPrototype> voice,
Gender gender,
HumanoidCharacterAppearance appearance,
SpawnPriorityPreference spawnPriority,
Dictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
HashSet<ProtoId<AntagPrototype>> antagPreferences,
HashSet<ProtoId<TraitPrototype>> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
{
Name = name;
FlavorText = flavortext;
Species = species;
TTSVoice = TTS_voice; // Corvax-TTS
Age = age;
Sex = sex;
Voice = voice;
Gender = gender;
Appearance = appearance;
SpawnPriority = spawnPriority;
_jobPriorities = jobPriorities;
PreferenceUnavailable = preferenceUnavailable;
_antagPreferences = antagPreferences;
_traitPreferences = traitPreferences;
_loadouts = loadouts;
var hasHighPrority = false;
foreach (var (key, value) in _jobPriorities)
{
if (value == JobPriority.Never)
_jobPriorities.Remove(key);
else if (value != JobPriority.High)
continue;
if (hasHighPrority)
_jobPriorities[key] = JobPriority.Medium;
hasHighPrority = true;
}
}
/// <summary>Copy constructor</summary>
public HumanoidCharacterProfile(HumanoidCharacterProfile other)
: this(other.Name,
other.FlavorText,
other.Species,
other.TTSVoice,
other.Age,
other.Sex,
other.Voice,
other.Gender,
other.Appearance.Clone(),
other.SpawnPriority,
new Dictionary<ProtoId<JobPrototype>, JobPriority>(other.JobPriorities),
other.PreferenceUnavailable,
new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
new Dictionary<string, RoleLoadout>(other.Loadouts))
{
}
/// <summary>
/// Get the default humanoid character profile, using internal constant values.
/// Defaults to <see cref="DefaultSpecies"/> for the species.
/// </summary>
/// <returns></returns>
public HumanoidCharacterProfile()
{
}
/// <summary>
/// Return a default character profile, based on species.
/// </summary>
/// <param name="species">The species to use in this default profile. The default species is <see cref="DefaultSpecies"/>.</param>
/// <param name="sex">Self explanatory.</param>
/// <returns>Humanoid character profile with default settings.</returns>
public static HumanoidCharacterProfile DefaultWithSpecies(ProtoId<SpeciesPrototype>? species = null, Sex? sex = null)
{
species ??= HumanoidCharacterProfile.DefaultSpecies;
sex ??= Sex.Male;
return new()
{
Species = species.Value,
Sex = sex.Value,
Appearance = HumanoidCharacterAppearance.DefaultWithSpecies(species.Value, sex.Value),
};
}
/// <summary>
/// An enum defining randomizable values in character editor.
/// </summary>
[Flags]
public enum RandomizeCfg
{
// profile
None = 0,
Name = 1 << 0,
Species = 1 << 1,
Age = 1 << 2,
Sex = 1 << 3,
Gender = 1 << 4,
// appearance
Eyes = 1 << 5,
Skin = 1 << 6,
Markings = 1 << 7,
}
/// <summary>
/// A randomize config that covers all possible values (including appearance).
/// </summary>
public const RandomizeCfg RandomizeConfigAll =
RandomizeCfg.Name
| RandomizeCfg.Species
| RandomizeCfg.Age
| RandomizeCfg.Sex
| RandomizeCfg.Gender
| RandomizeCfg.Eyes
| RandomizeCfg.Skin
| RandomizeCfg.Markings;
/// <summary>
/// Picks a random species from roundstart species.
/// <param name="ignoredSpecies">Species to exclude from randomizer.</param>
/// </summary>
public static SpeciesPrototype RandomSpecies(HashSet<string>? ignoredSpecies = null)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var random = IoCManager.Resolve<IRobustRandom>();
var pool = prototypeManager.EnumeratePrototypes<SpeciesPrototype>()
.Where(x => ignoredSpecies == null ? x.RoundStart : x.RoundStart && !ignoredSpecies.Contains(x.ID))
.ToArray();
var species = random.Pick(pool);
return species;
}
/// <summary>
/// Picks a random name using species and gender.
/// </summary>
public static string RandomName(SpeciesPrototype species, Gender gender)
{
var name = GetName(species.ID, gender);
return name;
}
/// <summary>
/// Picks a random age using species.
/// </summary>
public static int RandomAge(SpeciesPrototype species)
{
var random = IoCManager.Resolve<IRobustRandom>();
var age = random.Next(species.MinAge, species.OldAge);
return age;
}
/// <summary>
/// Picks a random sex using species.
/// </summary>
public static Sex RandomSex(SpeciesPrototype species)
{
var random = IoCManager.Resolve<IRobustRandom>();
var sex = random.Pick(species.Sexes);
return sex;
}
// Corvax-TTS-Start
public static string RandomTTS(ProtoId<EmoteSoundsPrototype> voicePrototype)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var random = IoCManager.Resolve<IRobustRandom>();
var voiceId = random.Pick(prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => CanHaveVoice(o, voicePrototype)).ToArray()
).ID;
return voiceId;
}
// Corvax-TTS-End
/// <summary>
/// Picks a random gender using species sex;
/// </summary>
public static Gender RandomGender(Sex sex)
{
var gender = Gender.Epicene;
switch (sex)
{
case Sex.Male:
gender = Gender.Male;
break;
case Sex.Female:
gender = Gender.Female;
break;
}
return gender;
}
/// <summary>
/// Generates a randomized character profile.
/// </summary>
/// <returns>A new character profile with values randomized</returns>
public static HumanoidCharacterProfile Random(HashSet<string>? ignoredSpecies = null)
{
var config = RandomizeConfigAll;
var baseProfile = new HumanoidCharacterProfile();
if (ignoredSpecies != null)
{
baseProfile.Species = RandomSpecies(ignoredSpecies);
config ^= RandomizeCfg.Species;
}
var profile = Random(config, baseProfile);
return profile;
}
/// <summary>
/// Generates a randomized character profile with selective randomizing.
/// </summary>
/// <param name="randomizeCfg">Which values to randomize.</param>
/// <param name="baseProfile">Profile to base the new profile on. Values that are not randomized will be taken from this profile.</param>
/// <returns>A new character profile with selected values randomized</returns>
public static HumanoidCharacterProfile Random(RandomizeCfg randomizeCfg, HumanoidCharacterProfile baseProfile)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var profile = new HumanoidCharacterProfile();
if ((randomizeCfg & RandomizeCfg.Species) != 0)
{
profile.Species = RandomSpecies();
}
else
{
profile.Species = DefaultSpecies;
if (prototypeManager.HasIndex(baseProfile.Species))
{
profile.Species = baseProfile.Species;
}
}
var speciesProto = prototypeManager.Index(profile.Species);
profile.Sex = (randomizeCfg & RandomizeCfg.Sex) != 0 ? RandomSex(speciesProto) : baseProfile.Sex;
profile.Voice = speciesProto.DefaultSoundsBySex[(int)profile.Sex];
profile.Gender = (randomizeCfg & RandomizeCfg.Gender) != 0 ? RandomGender(profile.Sex) : baseProfile.Gender;
profile.Name = (randomizeCfg & RandomizeCfg.Name) != 0 ? RandomName(speciesProto, profile.Gender) : baseProfile.Name;
profile.Age = (randomizeCfg & RandomizeCfg.Age) != 0 ? RandomAge(speciesProto) : baseProfile.Age;
profile.TTSVoice = (randomizeCfg & RandomizeCfg.Age) != 0 ? RandomTTS(profile.Voice) : baseProfile.TTSVoice; // Corvax-TTS
profile.Appearance = HumanoidCharacterAppearance.Random(speciesProto, profile.Sex, randomizeCfg, baseProfile.Appearance);
return profile;
}
/// <summary>
/// Generates a randomized character profile.
/// </summary>
/// <param name="species">Species to constrain randomizer to.</param>
/// <returns>A new character profile</returns>
public static HumanoidCharacterProfile RandomWithSpecies(string? species = null)
{
species ??= DefaultSpecies;
return Random(
RandomizeConfigAll ^ RandomizeCfg.Species,
new HumanoidCharacterProfile().WithSpecies(species)
);
}
public HumanoidCharacterProfile WithName(string name)
{
return new(this) { Name = name };
}
public HumanoidCharacterProfile WithFlavorText(string flavorText)
{
return new(this) { FlavorText = flavorText };
}
public HumanoidCharacterProfile WithAge(int age)
{
return new(this) { Age = age };
}
public HumanoidCharacterProfile WithSex(Sex sex)
{
return new(this) { Sex = sex };
}
public HumanoidCharacterProfile WithVoice(ProtoId<EmoteSoundsPrototype> voice)
{
return new(this) { Voice = voice };
}
public HumanoidCharacterProfile WithGender(Gender gender)
{
return new(this) { Gender = gender };
}
public HumanoidCharacterProfile WithSpecies(string species)
{
return new(this) { Species = species };
}
// Corvax-TTS-Start
public HumanoidCharacterProfile WithVoice(string voice)
{
return new(this) { TTSVoice = voice };
}
// Corvax-TTS-End
public HumanoidCharacterProfile WithCharacterAppearance(HumanoidCharacterAppearance appearance)
{
return new(this) { Appearance = appearance };
}
public HumanoidCharacterProfile WithSpawnPriorityPreference(SpawnPriorityPreference spawnPriority)
{
return new(this) { SpawnPriority = spawnPriority };
}
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<ProtoId<JobPrototype>, JobPriority>> jobPriorities)
{
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(jobPriorities);
var hasHighPrority = false;
foreach (var (key, value) in dictionary)
{
if (value == JobPriority.Never)
dictionary.Remove(key);
else if (value != JobPriority.High)
continue;
if (hasHighPrority)
dictionary[key] = JobPriority.Medium;
hasHighPrority = true;
}
return new(this)
{
_jobPriorities = dictionary
};
}
/// <summary>
/// Return a HumanoidCharacterProfile with only the job priorities listed in the NewCharacterJobs cvar
/// </summary>
public HumanoidCharacterProfile WithJobFromCvar(IConfigurationManager cfg)
{
// This path should run only rarely, so the cvar does not need to be locally stored
var jobs = new HashSet<string>(cfg.GetCVar(CCVars.NewCharacterJobs).Split(","));
var priority = JobPriority.High;
Dictionary<ProtoId<JobPrototype>, JobPriority> priorities = new();
foreach (var job in jobs)
{
// Remove whitespaces in case the input contained any
priorities.Add(job.Trim(), priority);
// There can be only one High priority
priority = JobPriority.Medium;
}
return new(this)
{
_jobPriorities = priorities,
};
}
public HumanoidCharacterProfile WithJobPriority(ProtoId<JobPrototype> jobId, JobPriority priority)
{
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(_jobPriorities);
if (priority == JobPriority.Never)
{
dictionary.Remove(jobId);
}
else if (priority == JobPriority.High)
{
// There can only ever be one high priority job.
foreach (var (job, value) in dictionary)
{
if (value == JobPriority.High)
dictionary[job] = JobPriority.Medium;
}
dictionary[jobId] = priority;
}
else
{
dictionary[jobId] = priority;
}
return new(this)
{
_jobPriorities = dictionary,
};
}
public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
{
return new(this) { PreferenceUnavailable = mode };
}
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<ProtoId<AntagPrototype>> antagPreferences)
{
return new(this)
{
_antagPreferences = new (antagPreferences),
};
}
public HumanoidCharacterProfile WithAntagPreference(ProtoId<AntagPrototype> antagId, bool pref)
{
var list = new HashSet<ProtoId<AntagPrototype>>(_antagPreferences);
if (pref)
{
list.Add(antagId);
}
else
{
list.Remove(antagId);
}
return new(this)
{
_antagPreferences = list,
};
}
public HumanoidCharacterProfile WithTraitPreference(ProtoId<TraitPrototype> traitId, IPrototypeManager protoManager)
{
// null category is assumed to be default.
if (!protoManager.TryIndex(traitId, out var traitProto))
return new(this);
var category = traitProto.Category;
// Category not found so dump it.
TraitCategoryPrototype? traitCategory = null;
if (category != null && !protoManager.Resolve(category, out traitCategory))
return new(this);
var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences) { traitId };
if (traitCategory == null || traitCategory.MaxTraitPoints < 0)
{
return new(this)
{
_traitPreferences = list,
};
}
var count = 0;
foreach (var trait in list)
{
// If trait not found or another category don't count its points.
if (!protoManager.TryIndex<TraitPrototype>(trait, out var otherProto) ||
otherProto.Category != traitCategory)
{
continue;
}
count += otherProto.Cost;
}
if (count > traitCategory.MaxTraitPoints && traitProto.Cost != 0)
{
return new(this);
}
return new(this)
{
_traitPreferences = list,
};
}
public HumanoidCharacterProfile WithoutTraitPreference(ProtoId<TraitPrototype> traitId, IPrototypeManager protoManager)
{
var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences);
list.Remove(traitId);
return new(this)
{
_traitPreferences = list,
};
}
public string Summary =>
Loc.GetString(
"humanoid-character-profile-summary",
("name", Name),
("gender", Gender.ToString().ToLowerInvariant()),
("age", Age)
);
public bool MemberwiseEquals(HumanoidCharacterProfile other)
{
if (Name != other.Name) return false;
if (Age != other.Age) return false;
if (Sex != other.Sex) return false;
if (Voice != other.Voice) return false;
if (Gender != other.Gender) return false;
if (Species != other.Species) return false;
if (PreferenceUnavailable != other.PreferenceUnavailable) return false;
if (SpawnPriority != other.SpawnPriority) return false;
if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false;
if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false;
if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
if (FlavorText != other.FlavorText) return false;
if (TTSVoice != other.TTSVoice) return false; // Corvax-TTS
return Appearance.Equals(other.Appearance);
}
public void EnsureValid(ICommonSession session, IDependencyCollection collection, string[] sponsorPrototypes)
{
var configManager = collection.Resolve<IConfigurationManager>();
var prototypeManager = collection.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
{
Species = HumanoidCharacterProfile.DefaultSpecies;
speciesPrototype = prototypeManager.Index(Species);
}
// Corvax-Sponsors-Start: Reset to human if player not sponsor
if (speciesPrototype.SponsorOnly && !sponsorPrototypes.Contains(Species.Id))
{
Species = HumanoidCharacterProfile.DefaultSpecies;
speciesPrototype = prototypeManager.Index(Species);
}
// Corvax-Sponsors-End
var sex = Sex switch
{
Sex.Male => Sex.Male,
Sex.Female => Sex.Female,
Sex.Unsexed => Sex.Unsexed,
_ => Sex.Male // Invalid enum values.
};
var voice = Voice;
if (!speciesPrototype.Voices.Contains(voice))
voice = speciesPrototype.DefaultSoundsBySex[(int)sex];
// ensure the species can be that sex and their age fits the founds
if (!speciesPrototype.Sexes.Contains(sex))
sex = speciesPrototype.Sexes[0];
var age = Math.Clamp(Age, speciesPrototype.MinAge, speciesPrototype.MaxAge);
var gender = Gender switch
{
Gender.Epicene => Gender.Epicene,
Gender.Female => Gender.Female,
Gender.Male => Gender.Male,
Gender.Neuter => Gender.Neuter,
_ => Gender.Epicene // Invalid enum values.
};
string name;
var maxNameLength = configManager.GetCVar(CCVars.MaxNameLength);
if (string.IsNullOrEmpty(Name))
{
name = GetName(Species, gender);
}
else if (Name.Length > maxNameLength)
{
name = Name[..maxNameLength];
}
else
{
name = Name;
}
name = name.Trim();
if (configManager.GetCVar(CCVars.RestrictedNames))
{
name = RestrictedNameRegex.Replace(name, string.Empty);
}
if (configManager.GetCVar(CCVars.ICNameCase))
{
// This regex replaces the first character of the first and last words of the name with their uppercase version
name = ICNameCaseRegex.Replace(name, m => m.Groups["word"].Value.ToUpper());
}
if (string.IsNullOrEmpty(name))
{
name = GetName(Species, gender);
}
string flavortext;
var maxFlavorTextLength = configManager.GetCVar(CCVars.MaxFlavorTextLength);
if (FlavorText.Length > maxFlavorTextLength)
{
flavortext = FormattedMessage.RemoveMarkupOrThrow(FlavorText)[..maxFlavorTextLength];
}
else
{
flavortext = FormattedMessage.RemoveMarkupOrThrow(FlavorText);
}
var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex, sponsorPrototypes);
var prefsUnavailableMode = PreferenceUnavailable switch
{
PreferenceUnavailableMode.StayInLobby => PreferenceUnavailableMode.StayInLobby,
PreferenceUnavailableMode.SpawnAsOverflow => PreferenceUnavailableMode.SpawnAsOverflow,
_ => PreferenceUnavailableMode.StayInLobby // Invalid enum values.
};
var spawnPriority = SpawnPriority switch
{
SpawnPriorityPreference.None => SpawnPriorityPreference.None,
SpawnPriorityPreference.Arrivals => SpawnPriorityPreference.Arrivals,
SpawnPriorityPreference.Cryosleep => SpawnPriorityPreference.Cryosleep,
_ => SpawnPriorityPreference.None // Invalid enum values.
};
var priorities = new Dictionary<ProtoId<JobPrototype>, JobPriority>(JobPriorities
.Where(p => prototypeManager.TryIndex<JobPrototype>(p.Key, out var job) && job.SetPreference && p.Value switch
{
JobPriority.Never => false, // Drop never since that's assumed default.
JobPriority.Low => true,
JobPriority.Medium => true,
JobPriority.High => true,
_ => false
}));
var hasHighPrio = false;
foreach (var (key, value) in priorities)
{
if (value != JobPriority.High)
continue;
if (hasHighPrio)
priorities[key] = JobPriority.Medium;
hasHighPrio = true;
}
var antags = AntagPreferences
.Where(id => prototypeManager.TryIndex(id, out var antag) && antag.SetPreference)
.ToList();
var traits = TraitPreferences
.Where(prototypeManager.HasIndex)
.ToList();
Name = name;
FlavorText = flavortext;
Age = age;
Sex = sex;
Voice = voice;
Gender = gender;
Appearance = appearance;
SpawnPriority = spawnPriority;
_jobPriorities.Clear();
foreach (var (job, priority) in priorities)
{
_jobPriorities.Add(job, priority);
}
PreferenceUnavailable = prefsUnavailableMode;
_antagPreferences.Clear();
_antagPreferences.UnionWith(antags);
_traitPreferences.Clear();
_traitPreferences.UnionWith(GetValidTraits(traits, prototypeManager));
// Corvax-TTS-Start
prototypeManager.TryIndex<TTSVoicePrototype>(TTSVoice, out var TTS_voice);
if (TTS_voice is null || !CanHaveVoice(TTS_voice, Voice))
TTSVoice = HumanoidProfileSystem.DefaultSexVoice[sex];
// Corvax-TTS-End
// Checks prototypes exist for all loadouts and dump / set to default if not.
var toRemove = new ValueList<string>();
foreach (var (roleName, loadouts) in _loadouts)
{
if (!prototypeManager.HasIndex<RoleLoadoutPrototype>(roleName))
{
toRemove.Add(roleName);
continue;
}
// This happens after we verify the prototype exists
// These values are set equal in the database and we need to make sure they're equal here too!
loadouts.Role = roleName;
loadouts.EnsureValid(this, session, collection);
}
foreach (var value in toRemove)
{
_loadouts.Remove(value);
}
}
/// <summary>
/// Takes in an IEnumerable of traits and returns a List of the valid traits.
/// </summary>
public List<ProtoId<TraitPrototype>> GetValidTraits(IEnumerable<ProtoId<TraitPrototype>> traits, IPrototypeManager protoManager)
{
// Track points count for each group.
var groups = new Dictionary<string, int>();
var result = new List<ProtoId<TraitPrototype>>();
foreach (var trait in traits)
{
if (!protoManager.TryIndex(trait, out var traitProto))
continue;
// Always valid.
if (traitProto.Category == null)
{
result.Add(trait);
continue;
}
// No category so dump it.
if (!protoManager.Resolve(traitProto.Category, out var category))
continue;
var existing = groups.GetOrNew(category.ID);
existing += traitProto.Cost;
// Too expensive.
if (existing > category.MaxTraitPoints)
continue;
groups[category.ID] = existing;
result.Add(trait);
}
return result;
}
// Corvax-TTS-Start
// SHOULD BE NOT PUBLIC, BUT.... Похуй
public static bool CanHaveVoice(TTSVoicePrototype voice, ProtoId<EmoteSoundsPrototype> voicePrototype)
{
if (voice.Sex == Sex.Unsexed)
return true;
var protoId = voicePrototype.Id;
if (protoId.StartsWith("Unisex", StringComparison.OrdinalIgnoreCase))
return true;
return voice.Sex switch
{
Sex.Male => protoId.StartsWith("Male", StringComparison.OrdinalIgnoreCase),
Sex.Female => protoId.StartsWith("Female", StringComparison.OrdinalIgnoreCase),
_ => false
};
}
// Corvax-TTS-End
public HumanoidCharacterProfile Validated(ICommonSession session, IDependencyCollection collection, string[] sponsorPrototypes)// Corvax-Sponsors
{
var profile = new HumanoidCharacterProfile(this);
profile.EnsureValid(session, collection, sponsorPrototypes);
return profile;
}
// sorry this is kind of weird and duplicated,
/// working inside these non entity systems is a bit wack
public static string GetName(string species, Gender gender)
{
var namingSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<NamingSystem>();
return namingSystem.GetName(species, gender);
}
public bool Equals(HumanoidCharacterProfile? other)
{
if (other is null)
return false;
return ReferenceEquals(this, other) || MemberwiseEquals(other);
}
public override bool Equals(object? obj)
{
return obj is HumanoidCharacterProfile other && Equals(other);
}
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(_jobPriorities);
hashCode.Add(_antagPreferences);
hashCode.Add(_traitPreferences);
hashCode.Add(_loadouts);
hashCode.Add(Name);
hashCode.Add(FlavorText);
hashCode.Add(Species);
hashCode.Add(Age);
hashCode.Add((int)Sex);
hashCode.Add(Voice);
hashCode.Add(TTSVoice); // Corvax-TTS
hashCode.Add((int)Gender);
hashCode.Add(Appearance);
hashCode.Add((int)SpawnPriority);
hashCode.Add((int)PreferenceUnavailable);
return hashCode.ToHashCode();
}
public void SetLoadout(RoleLoadout loadout)
{
_loadouts[loadout.Role.Id] = loadout;
}
public HumanoidCharacterProfile WithLoadout(RoleLoadout loadout)
{
// Deep copies so we don't modify the DB profile.
var copied = new Dictionary<string, RoleLoadout>();
foreach (var proto in _loadouts)
{
if (proto.Key == loadout.Role)
continue;
copied[proto.Key] = proto.Value.Clone();
}
copied[loadout.Role] = loadout.Clone();
var profile = Clone();
profile._loadouts = copied;
return profile;
}
public RoleLoadout GetLoadoutOrDefault(string id, ICommonSession? session, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
{
if (!_loadouts.TryGetValue(id, out var loadout))
{
loadout = new RoleLoadout(id);
loadout.SetDefault(this, session, protoManager, force: true);
}
loadout.SetDefault(this, session, protoManager);
return loadout;
}
public HumanoidCharacterProfile Clone()
{
return new HumanoidCharacterProfile(this);
}
public DataNode ToDataNode(ISerializationManager? serialization = null, IConfigurationManager? configuration = null)
{
IoCManager.Resolve(ref serialization);
IoCManager.Resolve(ref configuration);
var export = new HumanoidProfileExportV2()
{
ForkId = configuration.GetCVar(CVars.BuildForkId),
Profile = this,
};
var dataNode = serialization.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
return dataNode;
}
public static HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session, ISerializationManager? serialization = null, IConfigurationManager? configuration = null)
{
IoCManager.Resolve(ref serialization);
IoCManager.Resolve(ref configuration);
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
var root = yamlStream.Documents[0].RootNode;
HumanoidCharacterProfile profile;
if (root["version"].Equals(new YamlScalarNode("1")))
{
var export = serialization.Read<HumanoidProfileExportV1>(root.ToDataNode(), notNullableOverride: true);
profile = export.ToV2().Profile;
}
else if (root["version"].Equals(new YamlScalarNode("2")))
{
var export = serialization.Read<HumanoidProfileExportV2>(root.ToDataNode(), notNullableOverride: true);
profile = export.Profile;
}
else
{
throw new InvalidOperationException($"Unknown version {root["version"]}");
}
var collection = IoCManager.Instance;
// Corvax-Sponsors-Start
string[] sponsorPrototypes;
try
{
var sponsorsManager = IoCManager.Resolve<ISharedSponsorsManager>();
sponsorPrototypes = sponsorsManager.TryGetServerPrototypes(session.UserId, out var prototypes)
? prototypes.ToArray() : Array.Empty<string>();
}
catch (Exception)
{
sponsorPrototypes = Array.Empty<string>();
}
profile.EnsureValid(session, collection!, sponsorPrototypes);
// Corvax-Sponsors-End
return profile;
}
}
}