using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Reflection.Emit; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using DooDesch.AvatarKit; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppScheduleOne; using Il2CppScheduleOne.AvatarFramework; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Map; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.PlayerScripts; using Il2CppScheduleOne.UI; using Il2CppSystem.Collections.Generic; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Personnel; using Personnel.Appearance; using Personnel.Config; using Personnel.Content; using Personnel.Model; using Personnel.Registration; using Personnel.Spawn; using Personnel.Util; using S1API.Casino; using S1API.Economy; using S1API.Entities; using S1API.Entities.Customer; using S1API.Entities.Dealer; using S1API.Entities.Relation; using S1API.Entities.Schedule; using S1API.Map; using S1API.Rendering; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "Personnel", "2.2.0", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Personnel")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("DooDesch")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © DooDesch")] [assembly: AssemblyFileVersion("2.2.0.0")] [assembly: AssemblyInformationalVersion("2.2.0+cf7fa11d9361c45be8f1d25e515b5cb36671b90b")] [assembly: AssemblyProduct("Personnel")] [assembly: AssemblyTitle("Personnel")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("2.2.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace DooDesch { internal static class ModVersion { internal const string Current = "2.2.0"; } } namespace DooDesch.AvatarKit { public static class AvatarDistortion { public static readonly string[] BoneKeys = new string[9] { "HeadBone", "HipBone", "LeftFootBone", "RightFootBone", "LeftShoulder", "RightShoulder", "MiddleSpine", "LowerSpine", "LowestSpine" }; public const string FaceMeshKey = "FaceMesh"; public const string BodyMeshKeyPrefix = "BodyMesh"; private static Transform BoneByName(Avatar avatar, string key) { return (Transform)(key switch { "HeadBone" => avatar.HeadBone, "HipBone" => avatar.HipBone, "LeftFootBone" => avatar.LeftFootBone, "RightFootBone" => avatar.RightFootBone, "LeftShoulder" => avatar.LeftShoulder, "RightShoulder" => avatar.RightShoulder, "MiddleSpine" => avatar.MiddleSpine, "LowerSpine" => avatar.LowerSpine, "LowestSpine" => avatar.LowestSpine, _ => null, }); } public static void Apply(Avatar avatar, IReadOnlyDictionary entries) { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)avatar == (Object)null) { return; } AvatarEffects component = ((Component)avatar).GetComponent(); if ((Object)(object)component != (Object)null) { ((Behaviour)component).enabled = entries == null || entries.Count == 0; } string[] boneKeys = BoneKeys; foreach (string key in boneKeys) { Transform val = BoneByName(avatar, key); if (!((Object)(object)val == (Object)null)) { if (entries != null && entries.TryGetValue(key, out (Vector3, bool) value)) { val.localScale = (value.Item2 ? Vector3.zero : value.Item1); } else { val.localScale = Vector3.one; } } } if ((Object)(object)avatar.FaceMesh != (Object)null) { (Vector3, bool) value2; bool flag = entries != null && entries.TryGetValue("FaceMesh", out value2) && value2.Item2; ((Renderer)avatar.FaceMesh).enabled = !flag; } Il2CppReferenceArray bodyMeshes = avatar.BodyMeshes; if (bodyMeshes == null) { return; } for (int j = 0; j < ((Il2CppArrayBase)(object)bodyMeshes).Length; j++) { if (!((Object)(object)((Il2CppArrayBase)(object)bodyMeshes)[j] == (Object)null)) { (Vector3, bool) value3; bool flag2 = entries != null && entries.TryGetValue("BodyMesh" + j, out value3) && value3.Item2; ((Renderer)((Il2CppArrayBase)(object)bodyMeshes)[j]).enabled = !flag2; } } } } public static class AvatarLayerSlots { public const int BodySlots = 8; public const int VanillaClearedSlots = 6; public const int FaceSlots = 6; public const int FixedFaceEntries = 2; public const int FreeFaceEntries = 3; public const string EmptyFacePath = "Avatar/Layers/None"; private const string MouthPrefix = "Avatar/Layers/Face/Face_"; private const string FacialHairPrefix = "Avatar/Layers/Face/FacialHair"; public static bool IsMouthLayer(string path) { return path?.StartsWith("Avatar/Layers/Face/Face_", StringComparison.OrdinalIgnoreCase) ?? false; } public static bool IsFacialHairLayer(string path) { return path?.StartsWith("Avatar/Layers/Face/FacialHair", StringComparison.OrdinalIgnoreCase) ?? false; } public static void OrderFaceLayers(List list) { //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) if (list == null) { return; } List<(string, Color)> list2 = new List<(string, Color)>(); List<(string, Color)> list3 = new List<(string, Color)>(); List<(string, Color)> list4 = new List<(string, Color)>(); for (int i = 0; i < list.Count; i++) { LayerSetting val = list[i]; string layerPath = val.layerPath; if (!string.IsNullOrEmpty(layerPath) && !(layerPath == "Avatar/Layers/None")) { if (list2.Count == 0 && IsMouthLayer(layerPath)) { list2.Add((layerPath, val.layerTint)); } else if (list3.Count == 0 && IsFacialHairLayer(layerPath)) { list3.Add((layerPath, val.layerTint)); } else { list4.Add((layerPath, val.layerTint)); } } } list.Clear(); list.Add((list2.Count > 0) ? Setting(list2[0].Item1, list2[0].Item2) : Setting("Avatar/Layers/None", Color.white)); list.Add((list3.Count > 0) ? Setting(list3[0].Item1, list3[0].Item2) : Setting("Avatar/Layers/None", Color.white)); for (int j = 0; j < list4.Count; j++) { list.Add(Setting(list4[j].Item1, list4[j].Item2)); } } public static List TrimFaceToBudget(List list, Func rank) { List list2 = new List(); if (list == null || rank == null || list.Count <= 5) { return list2; } for (int num = CountRenderable(list, 2) - 3; num > 0; num--) { int num2 = -1; int num3 = int.MaxValue; for (int i = 2; i < list.Count; i++) { if (Renders(list[i].layerPath)) { int num4 = rank(list[i].layerPath); if (num4 < num3) { num3 = num4; num2 = i; } } } if (num2 < 0) { break; } list2.Add(list[num2]); list.RemoveAt(num2); } return list2; } private static LayerSetting Setting(string path, Color tint) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown return new LayerSetting { layerPath = path, layerTint = tint }; } public static void LoadAndClean(Avatar avatar, AvatarSettings settings) { if (!((Object)(object)avatar == (Object)null) && !((Object)(object)settings == (Object)null)) { avatar.LoadAvatarSettings(settings); List bodyLayerSettings = settings.BodyLayerSettings; int used = ((bodyLayerSettings != null && bodyLayerSettings.Count > 6) ? CountRenderable(bodyLayerSettings) : 6); ClearStaleBodySlots(avatar, used); } } public static int CountRenderable(List list, int startIndex = 0) { if (list == null) { return 0; } int num = 0; for (int i = Math.Max(0, startIndex); i < list.Count; i++) { if (Renders(list[i].layerPath)) { num++; } } return num; } public static void ClearStaleBodySlots(Avatar avatar, int used) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)avatar == (Object)null || used >= 8) { return; } if (used < 0) { used = 0; } try { Il2CppReferenceArray bodyMeshes = avatar.BodyMeshes; for (int i = used + 1; i <= 8; i++) { avatar.SetBodyLayer(i, string.Empty, Color.white); if (bodyMeshes == null) { continue; } for (int j = 0; j < ((Il2CppArrayBase)(object)bodyMeshes).Length; j++) { SkinnedMeshRenderer val = ((Il2CppArrayBase)(object)bodyMeshes)[j]; if ((Object)(object)val != (Object)null && (Object)(object)((Renderer)val).material != (Object)null) { ((Renderer)val).material.SetTexture("_Layer_" + i + "_Normal", (Texture)null); } } } } catch (Exception) { } } public static List TrimToBudget(List list, Func rank, int max = 8) { List list2 = new List(); if (list == null || rank == null || list.Count <= max) { return list2; } for (int num = CountRenderable(list) - max; num > 0; num--) { int num2 = -1; int num3 = int.MaxValue; for (int i = 0; i < list.Count; i++) { if (Renders(list[i].layerPath)) { int num4 = rank(list[i].layerPath); if (num4 < num3) { num3 = num4; num2 = i; } } } if (num2 < 0) { break; } list2.Add(list[num2]); list.RemoveAt(num2); } return list2; } public static bool AddOnce(List list, string path, Color tint) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) if (list == null || string.IsNullOrEmpty(path)) { return false; } for (int i = 0; i < list.Count; i++) { if (list[i].layerPath == path) { return false; } } LayerSetting val = new LayerSetting(); val.layerPath = path; val.layerTint = tint; list.Add(val); return true; } private static bool Renders(string layerPath) { if (!string.IsNullOrEmpty(layerPath)) { return Resources.Load(layerPath) != (Object)null; } return false; } } } namespace Personnel { public static class API { private static MethodInfo _randomImpostor; private static bool _randomImpostorProbed; public static IReadOnlyList All => NpcRegistry.AllDefs; public static event Action OnReloaded { add { NpcRegistry.OnReloaded += value; } remove { NpcRegistry.OnReloaded -= value; } } public static bool TryGet(string id, out NpcDef def) { return NpcRegistry.TryGet(id, out def); } public static bool Register(NpcDef def) { return NpcRegistry.Add(def); } public static AvatarSettings BuildAvatarSettings(NpcDef def) { return AvatarSettingsFactory.BuildAvatarSettings(def); } public static bool ApplyAppearance(Avatar avatar, NpcDef def) { if ((Object)(object)avatar == (Object)null || def == null) { return false; } AvatarSettings val = BuildAvatarSettings(def); if ((Object)(object)val == (Object)null) { return false; } AvatarLayerSlots.LoadAndClean(avatar, val); return true; } public static void ApplyDistortion(Avatar avatar, NpcDef def) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)avatar == (Object)null || def?.Appearance == null) { return; } Dictionary dictionary = new Dictionary(); foreach (KeyValuePair item in def.Appearance.Distortion) { if (item.Value != null) { dictionary[item.Key] = (item.Value.Scale, item.Value.Hide); } } AvatarDistortion.Apply(avatar, dictionary); } public static void Reload() { NpcRegistry.Reload(); } public static void ConfigureFromDef(NPCPrefabBuilder builder, NpcDef def) { //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01be: Unknown result type (might be due to invalid IL or missing references) if (builder == null || def == null) { return; } SplitName(def.DisplayName, out var first, out var last); builder.WithIdentity(def.SaveId ?? def.Id, first, last); builder.WithAppearanceDefaults((Action)delegate(AvatarDefaultsBuilder ab) { AvatarSettingsFactory.ApplyToDefaults(ab, def); ApplyImpostorIfSupported(ab, def); }); bool unlocked = def.Relationships?.Unlocked ?? def.Contact?.Visible ?? true; builder.WithRelationshipDefaults((Action)delegate(NPCRelationshipDataBuilder r) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) r.SetUnlocked(unlocked); NpcRelationships relationships = def.Relationships; if (relationships != null) { if (relationships.Delta.HasValue) { r.WithDelta(relationships.Delta.Value); } if (!string.IsNullOrWhiteSpace(relationships.UnlockType)) { if (Parse.TryParseEnum(relationships.UnlockType, out UnlockType result)) { r.SetUnlockType(result); } else { Instance log = Core.Log; if (log != null) { log.Warning($"'{def.Id}': unknown relationships.unlockType '{relationships.UnlockType}' - ignored."); } } } if (relationships.Connections != null && relationships.Connections.Count > 0) { r.WithConnectionsById((IEnumerable)relationships.Connections); } } }); string text = ResolveRole(def); if (text == "dealer") { ApplyDealer(builder, def); } else if (text == "customer") { ApplyCustomer(builder, def); } ApplyInventory(builder, def); List list = ScheduleSpecFactory.Build(def); if (list.Count > 0) { builder.WithSchedule((IEnumerable)list); } NpcSpawn spawn = def.Spawn; if (spawn != null && spawn.Position.HasValue) { Quaternion val = (def.Spawn.RotationY.HasValue ? Quaternion.Euler(0f, def.Spawn.RotationY.Value, 0f) : Quaternion.identity); builder.WithSpawnPosition(def.Spawn.Position.Value, val); } } internal static string ResolveRole(NpcDef def) { if (def == null) { return null; } string a = def.Behavior?.Conversation; bool flag = string.Equals(a, "dealer", StringComparison.OrdinalIgnoreCase); bool flag2 = string.Equals(a, "customer", StringComparison.OrdinalIgnoreCase); if (def.Dealer != null) { if (def.Customer != null || flag2) { Instance log = Core.Log; if (log != null) { log.Warning("'" + def.Id + "': has both dealer and customer data - dealer wins (an NPC can only be one)."); } } return "dealer"; } if (def.Customer != null) { if (flag) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("'" + def.Id + "': customer{} block contradicts behavior.conversation=\"dealer\" - customer wins."); } } return "customer"; } if (flag) { return "dealer"; } if (flag2) { return "customer"; } return null; } private static void ApplyCustomer(NPCPrefabBuilder builder, NpcDef def) { builder.WithCustomerDefaults((Action)delegate(CustomerDataBuilder c) { NpcCustomer customer = def.Customer; if (customer != null) { if (customer.Spending != null) { c.WithSpending(customer.Spending.Min, customer.Spending.Max); } if (customer.OrdersPerWeek != null) { c.WithOrdersPerWeek((int)customer.OrdersPerWeek.Min, (int)customer.OrdersPerWeek.Max); } if (!string.IsNullOrWhiteSpace(customer.PreferredOrderDay)) { c.WithPreferredOrderDay(customer.PreferredOrderDay); } if (customer.OrderTime.HasValue) { c.WithOrderTime(customer.OrderTime.Value); } if (!string.IsNullOrWhiteSpace(customer.Standards)) { c.WithStandards(customer.Standards); } if (customer.AllowDirectApproach.HasValue) { c.AllowDirectApproach(customer.AllowDirectApproach.Value); } if (customer.GuaranteeFirstSample.HasValue) { c.GuaranteeFirstSample(customer.GuaranteeFirstSample.Value); } if (customer.MutualRelationRequirement != null) { c.WithMutualRelationRequirement(customer.MutualRelationRequirement.Min, customer.MutualRelationRequirement.Max); } if (customer.CallPoliceChance.HasValue) { c.WithCallPoliceChance(customer.CallPoliceChance.Value); } if (customer.DependenceBase.HasValue) { c.WithDependence(customer.DependenceBase.Value, customer.DependenceMultiplier ?? 1f); } if (customer.Affinities != null && customer.Affinities.Count > 0) { List<(string, float)> list = new List<(string, float)>(); foreach (KeyValuePair affinity in customer.Affinities) { list.Add((affinity.Key, affinity.Value)); } c.WithAffinities((IEnumerable>)list); } if (customer.PreferredProperties != null && customer.PreferredProperties.Count > 0) { c.WithPreferredPropertiesById(customer.PreferredProperties.ToArray()); } } }); } private static void ApplyDealer(NPCPrefabBuilder builder, NpcDef def) { NpcDealer de = def.Dealer; if (de == null || (de.Type == null && !de.Cut.HasValue && !de.SigningFee.HasValue && de.Home == null && de.CompletedDealsVariable == null && !de.AllowInsufficientQuality.HasValue && !de.AllowExcessQuality.HasValue)) { return; } builder.WithDealerDefaults((Action)delegate(DealerDataBuilder d) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) if (de != null) { if (!string.IsNullOrWhiteSpace(de.Type)) { string text = de.Type; if (string.Equals(text, "player", StringComparison.OrdinalIgnoreCase)) { text = "PlayerDealer"; } else if (string.Equals(text, "cartel", StringComparison.OrdinalIgnoreCase)) { text = "CartelDealer"; } if (Parse.TryParseEnum(text, out DealerType result)) { d.WithDealerType(result); } else { Instance log = Core.Log; if (log != null) { log.Warning($"'{def.Id}': unknown dealer.type '{de.Type}' - ignored."); } } } if (de.Cut.HasValue) { d.WithCut(de.Cut.Value); } if (de.SigningFee.HasValue) { d.WithSigningFee(de.SigningFee.Value); } if (!string.IsNullOrWhiteSpace(de.Home)) { d.WithHomeName(de.Home); } if (!string.IsNullOrWhiteSpace(de.CompletedDealsVariable)) { d.WithCompletedDealsVariable(de.CompletedDealsVariable); } if (de.AllowInsufficientQuality.HasValue) { d.AllowInsufficientQuality(de.AllowInsufficientQuality.Value); } if (de.AllowExcessQuality.HasValue) { d.AllowExcessQuality(de.AllowExcessQuality.Value); } } }); } private static void ApplyImpostorIfSupported(AvatarDefaultsBuilder ab, NpcDef def) { try { if (!_randomImpostorProbed) { _randomImpostorProbed = true; _randomImpostor = typeof(AvatarDefaultsBuilder).GetMethod("WithRandomImpostor", new Type[2] { typeof(int), typeof(string[]) }); } _randomImpostor?.Invoke(ab, new object[2] { Parse.StableHash(def.Id), Array.Empty() }); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning($"'{def.Id}': setting an impostor failed ({ex.Message}) - distant billboard may be blank."); } } } private static void ApplyInventory(NPCPrefabBuilder builder, NpcDef def) { NpcInventory inv = def.Inventory; if (inv == null) { return; } builder.WithInventoryDefaults((Action)delegate(RandomInventoryItemsBuilder i) { if (inv.Cash != null) { i.WithRandomCash((int)inv.Cash.Min, (int)inv.Cash.Max); } if (inv.Items != null) { foreach (NpcInventoryItem item in inv.Items) { for (int j = 0; j < item.Quantity; j++) { i.WithStartupItem(item.Id); } } } if (inv.ClearEachNight.HasValue) { i.WithClearInventoryEachNight(inv.ClearEachNight.Value); } }); } public static void AddMapMarker(GameObject npcRoot) { if ((Object)(object)npcRoot == (Object)null || !NetworkSingleton.InstanceExists) { return; } NPCManager instance = NetworkSingleton.Instance; if (!((Object)(object)instance == (Object)null) && !((Object)(object)instance.NPCPoIPrefab == (Object)null)) { NPC component = npcRoot.GetComponent(); if (!((Object)(object)component == (Object)null)) { NPCPoI obj = Object.Instantiate(instance.NPCPoIPrefab, npcRoot.transform); obj.SetNPC(component); ((Behaviour)obj).enabled = true; } } } private static void SplitName(string display, out string first, out string last) { first = "NPC"; last = string.Empty; if (!string.IsNullOrWhiteSpace(display)) { display = display.Trim(); int num = display.IndexOf(' '); if (num > 0) { first = display.Substring(0, num); last = display.Substring(num + 1).Trim(); } else { first = display; } } } } public sealed class Core : MelonMod { public static Core Instance { get; private set; } public static Instance Log { get; private set; } public override void OnInitializeMelon() { Instance = this; Log = ((MelonBase)this).LoggerInstance; Preferences.Initialize(); ExamplePack.ExtractIfEnabled(); int packDefs = NpcRegistry.LoadPacks(); if (Preferences.EnableAutoRegister) { DynamicNpcTypeFactory.EmitAutoRegisteredTypes(NpcRegistry.AllDefs); } try { ((MelonBase)this).HarmonyInstance.PatchAll(); } catch (Exception ex) { Log.Warning("Console commands unavailable: " + ex.Message); } LogRosterSummary(packDefs); Log.Msg("Drop packs in: " + PackLoader.PacksRoot); Log.Msg("Writing a schedule? Type 'personnel help' in the dev console to grab coordinates."); } private static void LogRosterSummary(int packDefs) { int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; int num6 = 0; foreach (NpcDef allDef in NpcRegistry.AllDefs) { bool flag = allDef.Schedule != null && allDef.Schedule.Count > 0; if (allDef.Spawn?.Physical ?? flag) { num++; } else { num2++; } string text = API.ResolveRole(allDef); if (text == "customer") { num3++; } else if (text == "dealer") { num4++; } if (flag) { num5++; } NpcSpawn spawn = allDef.Spawn; if (spawn != null && spawn.Auto) { num6++; } } Log.Msg($"Personnel {((MelonBase)Instance).Info.Version} - {packDefs} NPC def(s) from packs ({NpcRegistry.AllDefs.Count} total): {num} physical / {num2} contact-only, {num3} customer(s), {num4} dealer(s), {num5} with schedules, {num6} auto-registered."); } } public abstract class PersonnelNpc : NPC { protected abstract string DefId { get; } public override bool IsDealer { get { try { NpcDef def; return API.TryGet(DefId, out def) && API.ResolveRole(def) == "dealer"; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning($"IsDealer lookup for '{((object)this).GetType().Name}' threw ({ex.Message}) - assuming civilian."); } return false; } } } public override bool IsPhysical { get { try { if (!API.TryGet(DefId, out var def) || def == null) { return false; } return def.Spawn?.Physical ?? (def.Schedule != null && def.Schedule.Count > 0); } catch { return false; } } } protected override void ConfigurePrefab(NPCPrefabBuilder builder) { if (API.TryGet(DefId, out var def) && def != null) { API.ConfigureFromDef(builder, def); return; } Instance log = Core.Log; if (log != null) { log.Warning("PersonnelNpc: no definition '" + DefId + "' found - is the pack installed?"); } } protected override void OnCreated() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) ((NPC)this).OnCreated(); if (!API.TryGet(DefId, out var def) || def == null) { API.AddMapMarker(((NPC)this).gameObject); return; } Avatar componentInChildren = ((NPC)this).gameObject.GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { API.ApplyDistortion(componentInChildren, def); } if (!string.IsNullOrWhiteSpace(def.Spawn?.Region)) { if (Parse.TryParseEnum(def.Spawn.Region, out Region result)) { ((NPC)this).Region = result; } else { Instance log = Core.Log; if (log != null) { log.Warning($"'{def.Id}': unknown spawn.region '{def.Spawn.Region}' - ignored."); } } } if (def.Behavior != null) { try { ((NPC)this).Aggressiveness = def.Behavior.Aggression; ((NPC)this).MaxHealth = def.Behavior.MaxHealth; if (def.Behavior.Scale > 0f) { ((NPC)this).Scale = def.Behavior.Scale; } } catch (Exception ex) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning($"'{def.Id}': applying behavior stats failed ({ex.Message})."); } } } if (def.Schedule != null && def.Schedule.Count > 0) { try { ((NPC)this).Schedule.Enable(); } catch (Exception ex2) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning($"'{def.Id}': Schedule.Enable failed ({ex2.Message})."); } } } NpcContact contact = def.Contact; if (contact == null || contact.MapMarker != false) { API.AddMapMarker(((NPC)this).gameObject); } } } } namespace Personnel.Util { public static class ColorParse { public static bool TryParse(string s, out Color color) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) color = Color.white; if (string.IsNullOrWhiteSpace(s)) { return false; } s = s.Trim(); if (s.StartsWith("#")) { s = s.Substring(1); } if (s.Length != 6 && s.Length != 8) { return false; } try { byte b = Convert.ToByte(s.Substring(0, 2), 16); byte b2 = Convert.ToByte(s.Substring(2, 2), 16); byte b3 = Convert.ToByte(s.Substring(4, 2), 16); byte b4 = ((s.Length == 8) ? Convert.ToByte(s.Substring(6, 2), 16) : byte.MaxValue); color = Color32.op_Implicit(new Color32(b, b2, b3, b4)); return true; } catch { return false; } } public static Color Parse(string s, Color fallback) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (!TryParse(s, out var color)) { return fallback; } return color; } } public static class Ids { public static string Normalize(string s) { if (string.IsNullOrWhiteSpace(s)) { return ""; } StringBuilder stringBuilder = new StringBuilder(s.Length); bool flag = false; string text = s.Trim().ToLowerInvariant(); foreach (char c in text) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(c); flag = false; } else if (!flag) { stringBuilder.Append('_'); flag = true; } } return stringBuilder.ToString().Trim('_'); } public static string Make(string pack, string name) { string text = Normalize(pack); string text2 = Normalize(name); if (string.IsNullOrEmpty(text)) { return text2; } if (string.IsNullOrEmpty(text2)) { return text; } return text + "_" + text2; } } internal static class Parse { public static bool TryParseEnum(string value, out T result) where T : struct, Enum { result = default(T); if (string.IsNullOrWhiteSpace(value)) { return false; } string text = Fold(value); string[] names = Enum.GetNames(typeof(T)); foreach (string text2 in names) { if (Fold(text2) == text) { result = (T)Enum.Parse(typeof(T), text2); return true; } } return false; } private static string Fold(string s) { StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } public static bool TryParseTime(string value, out int hhmm) { hhmm = 0; if (string.IsNullOrWhiteSpace(value)) { return false; } value = value.Trim(); int num = value.IndexOf(':'); int result; int result2; if (num > 0) { if (!int.TryParse(value.Substring(0, num), out result)) { return false; } if (!int.TryParse(value.Substring(num + 1), out result2)) { return false; } } else { if (!int.TryParse(value, out var result3) || result3 < 0) { return false; } result = result3 / 100; result2 = result3 % 100; } if (result < 0 || result > 23 || result2 < 0 || result2 > 59) { return false; } hhmm = result * 100 + result2; return true; } public static bool TryParseVec3(float[] xyz, out Vector3 v) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) v = default(Vector3); if (xyz == null || xyz.Length != 3) { return false; } if (float.IsNaN(xyz[0]) || float.IsNaN(xyz[1]) || float.IsNaN(xyz[2])) { return false; } v = new Vector3(xyz[0], xyz[1], xyz[2]); return true; } public static int StableHash(string s) { uint num = 2166136261u; if (s != null) { foreach (char c in s) { num ^= c; num *= 16777619; } } return (int)num; } } } namespace Personnel.Tools { internal static class PersonnelConsole { private const string Prefix = "personnel"; private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; private static int _lastFrame = -1; private static string _lastSig = ""; private static string RoutePath => Path.Combine(MelonEnvironment.UserDataDirectory, "Personnel", "route.json"); internal static bool TryHandle(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return false; } return Dispatch(raw.Trim().Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)); } internal static bool TryHandle(List args) { if (args == null || args.Count == 0) { return false; } string[] array = new string[args.Count]; for (int i = 0; i < args.Count; i++) { array[i] = args[i]; } return Dispatch(array); } private static bool Dispatch(string[] p) { if (p.Length == 0 || !p[0].Equals("personnel", StringComparison.OrdinalIgnoreCase)) { return false; } string text = string.Join(" ", p); int frameCount = Time.frameCount; if (frameCount == _lastFrame && text == _lastSig) { return true; } _lastFrame = frameCount; _lastSig = text; string text2 = ((p.Length > 1) ? p[1].ToLowerInvariant() : "help"); try { switch (text2) { case "help": Help(); break; case "pos": Pos(Arg(p, 2)); break; case "spawn": SpawnBlock(); break; case "route": Route(Arg(p, 2)); break; case "npcs": Npcs(Arg(p, 2)); break; default: Log("unknown command '" + text2 + "'. Try: personnel help"); break; } } catch (Exception ex) { Log("command failed: " + ex.Message); } return true; } private static void Help() { Log("commands (results are copied to your clipboard and written to this log):"); Log(" personnel pos [HH:MM] position you are standing on - with a time it becomes a walkTo action"); Log(" personnel spawn the same spot as a spawn block (x/y/z/rotationY/region)"); Log(" personnel route HH:MM append a walkTo step to " + RoutePath); Log(" personnel route show print the collected steps"); Log(" personnel route clear start a new route"); Log(" personnel npcs [filter] list loaded Personnel NPCs, and where the physical ones are right now"); Notify("Personnel", "Command list written to the MelonLoader log."); } private static void Pos(string timeArg) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) if (TryGetPlayer(out var pos, out var yaw)) { string time; if (string.IsNullOrEmpty(timeArg)) { string text = Vec(pos); Log("position " + text + " (facing " + Num(yaw) + " degrees)"); Log(" paste into any action that takes a position: \"position\": " + text); Copy(text); Notify("Position copied", Plain(pos)); } else if (!TryNormalizeTime(timeArg, out time)) { Log("'" + timeArg + "' is not a time. Use HH:MM, e.g. personnel pos 07:30"); } else { string text2 = WalkTo(time, pos); Log("walkTo step at " + time + ":"); Log(" " + text2); Copy(text2); Notify("walkTo copied", time + " " + Plain(pos)); } } } private static void SpawnBlock() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) if (TryGetPlayer(out var pos, out var yaw)) { string text = RegionAt(pos); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("\"spawn\": { \"x\": ").Append(Num(pos.x)).Append(", \"y\": ") .Append(Num(pos.y)) .Append(", \"z\": ") .Append(Num(pos.z)) .Append(", \"rotationY\": ") .Append(Num(yaw)); if (text != null) { stringBuilder.Append(", \"region\": \"").Append(text).Append('"'); } stringBuilder.Append(", \"physical\": true }"); string text2 = stringBuilder.ToString(); Log("spawn block:"); Log(" " + text2); Copy(text2); Notify("Spawn block copied", Plain(pos) + ((text != null) ? (" " + text) : "")); } } private static void Route(string arg) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) string time; Vector3 pos; float yaw; if (string.Equals(arg, "show", StringComparison.OrdinalIgnoreCase)) { RouteShow(); } else if (string.Equals(arg, "clear", StringComparison.OrdinalIgnoreCase)) { RouteClear(); } else if (string.IsNullOrEmpty(arg)) { Log("personnel route HH:MM (or: personnel route show | personnel route clear)"); } else if (!TryNormalizeTime(arg, out time)) { Log("'" + arg + "' is not a time. Use HH:MM, e.g. personnel route 07:30"); } else if (TryGetPlayer(out pos, out yaw)) { string text = WalkTo(time, pos); try { Directory.CreateDirectory(Path.GetDirectoryName(RoutePath)); File.AppendAllText(RoutePath, text + "," + Environment.NewLine); } catch (Exception ex) { Log("could not write " + RoutePath + ": " + ex.Message); return; } int value = CountRouteSteps(); Log($"route step {value} at {time}: {text}"); Log(" file: " + RoutePath); Notify("Route step " + value, time + " " + Plain(pos)); } } private static void RouteShow() { string[] array = ReadRoute(); if (array.Length == 0) { Log("no route steps yet - walk somewhere and run: personnel route 07:30"); Notify("Personnel", "Route is empty."); return; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].TrimEnd()); if (i < array.Length - 1) { stringBuilder.Append(','); } stringBuilder.Append(Environment.NewLine); } string text = stringBuilder.ToString(); Log($"route ({array.Length} step(s)) - paste into \"schedule\": [ ... ]:"); string[] array2 = text.Split('\n'); foreach (string text2 in array2) { Log(" " + text2.TrimEnd()); } Copy(text); Notify("Route copied", array.Length + " step(s)"); } private static void RouteClear() { try { if (File.Exists(RoutePath)) { File.Delete(RoutePath); } } catch (Exception ex) { Log("could not clear " + RoutePath + ": " + ex.Message); return; } Log("route cleared."); Notify("Personnel", "Route cleared."); } private static string[] ReadRoute() { try { if (!File.Exists(RoutePath)) { return Array.Empty(); } List list = new List(); string[] array = File.ReadAllLines(RoutePath); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim().TrimEnd(','); if (text.Length > 0) { list.Add(text); } } return list.ToArray(); } catch { return Array.Empty(); } } private static int CountRouteSteps() { return ReadRoute().Length; } private static void Npcs(string filter) { //IL_0121: Unknown result type (might be due to invalid IL or missing references) IReadOnlyList allDefs = NpcRegistry.AllDefs; if (allDefs == null || allDefs.Count == 0) { Log("no NPC definitions loaded. Packs live in " + PackLoader.PacksRoot); Notify("Personnel", "No NPC definitions loaded."); return; } Dictionary dictionary = LivePositions(); int num = 0; Log($"{allDefs.Count} definition(s) loaded:"); foreach (NpcDef item in allDefs) { if (item != null && (string.IsNullOrEmpty(filter) || (item.Id != null && item.Id.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0))) { bool flag = item.Schedule != null && item.Schedule.Count > 0; bool flag2 = item.Spawn?.Physical ?? flag; Vector3 value2; string value = ((flag2 && dictionary.TryGetValue(item.Id ?? "", out value2)) ? (" at " + Plain(value2)) : ""); Log($" {item.Id} ({item.Source}) {(flag2 ? "physical" : "contact-only")}{(flag ? (", " + item.Schedule.Count + " schedule step(s)") : "")}{value}"); num++; } } if (num == 0) { Log(" (nothing matched '" + filter + "')"); } Notify("Personnel", num + " of " + allDefs.Count + " definition(s) listed."); } private static Dictionary LivePositions() { //IL_0046: Unknown result type (might be due to invalid IL or missing references) Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); try { List all = NPC.All; if (all == null) { return dictionary; } foreach (NPC item in all) { if (item == null) { continue; } try { string iD = item.ID; if (!string.IsNullOrEmpty(iD)) { dictionary[iD] = item.Position; } } catch { } } } catch { } return dictionary; } private static bool TryGetPlayer(out Vector3 pos, out float yaw) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) pos = Vector3.zero; yaw = 0f; Player val = null; try { val = Player.Local; } catch { } if ((Object)(object)val == (Object)null || (Object)(object)((Component)val).transform == (Object)null) { Log("no local player - load into a save first."); return false; } pos = ((Component)val).transform.position; yaw = ((Component)val).transform.eulerAngles.y; return true; } private static string RegionAt(Vector3 pos) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) try { Map instance = Singleton.Instance; return ((Object)(object)instance == (Object)null) ? null : ((object)instance.GetRegionFromPosition(pos)/*cast due to .constrained prefix*/).ToString(); } catch { return null; } } private static string WalkTo(string time, Vector3 pos) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) return "{ \"type\": \"walkTo\", \"time\": \"" + time + "\", \"position\": " + Vec(pos) + ", \"warpIfSkipped\": true }"; } private static bool TryNormalizeTime(string raw, out string time) { time = null; if (string.IsNullOrWhiteSpace(raw)) { return false; } string text = raw.Trim(); int num = text.IndexOf(':'); int result; int result2; if (num > 0) { if (!int.TryParse(text.Substring(0, num), NumberStyles.Integer, Inv, out result)) { return false; } if (!int.TryParse(text.Substring(num + 1), NumberStyles.Integer, Inv, out result2)) { return false; } } else { if (!int.TryParse(text, NumberStyles.Integer, Inv, out var result3)) { return false; } result = result3 / 100; result2 = result3 % 100; } if (result < 0 || result > 23 || result2 < 0 || result2 > 59) { return false; } time = result.ToString("00", Inv) + ":" + result2.ToString("00", Inv); return true; } private static string Num(float f) { return f.ToString("0.##", Inv); } private static string Vec(Vector3 v) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) return "[" + Num(v.x) + ", " + Num(v.y) + ", " + Num(v.z) + "]"; } private static string Plain(Vector3 v) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) return Num(v.x) + ", " + Num(v.y) + ", " + Num(v.z); } private static string Arg(string[] p, int i) { if (p.Length <= i) { return null; } return p[i]; } private static void Copy(string text) { try { GUIUtility.systemCopyBuffer = text; } catch (Exception ex) { Log("clipboard unavailable (" + ex.Message + ") - copy it from this log instead."); } } private static void Notify(string title, string subtitle) { try { NotificationsManager instance = Singleton.Instance; if ((Object)(object)instance != (Object)null) { instance.SendNotification(title, subtitle, (Sprite)null, 5f, false); } } catch { } } private static void Log(string msg) { Instance log = Core.Log; if (log != null) { log.Msg(msg); } } } [HarmonyPatch(typeof(Console), "SubmitCommand", new Type[] { typeof(string) })] internal static class Personnel_Console_SubmitCommand_String_Patch { private static bool Prefix(string args) { try { return !PersonnelConsole.TryHandle(args); } catch { return true; } } } [HarmonyPatch(typeof(Console), "SubmitCommand", new Type[] { typeof(List) })] internal static class Personnel_Console_SubmitCommand_List_Patch { private static bool Prefix(List args) { try { return !PersonnelConsole.TryHandle(args); } catch { return true; } } } } namespace Personnel.Spawn { internal static class DynamicNpcTypeFactory { private const string AssemblyName = "Personnel.Generated"; private static ModuleBuilder _module; private static readonly HashSet _emittedIds = new HashSet(StringComparer.OrdinalIgnoreCase); public static int EmitAutoRegisteredTypes(IReadOnlyList allDefs) { if (allDefs == null) { return 0; } List list = new List(); foreach (NpcDef allDef in allDefs) { if (allDef?.Spawn != null && allDef.Spawn.Auto && !_emittedIds.Contains(allDef.Id)) { list.Add(allDef); } } if (list.Count == 0) { return 0; } list.Sort((NpcDef a, NpcDef b) => string.CompareOrdinal(a.Id, b.Id)); CollectExistingConsumers(out var takenDefIds, out var takenTypeNames); int num = 0; foreach (NpcDef item in list) { if (takenDefIds.Contains(item.Id)) { Instance log = Core.Log; if (log != null) { log.Warning("auto-register: a compiled mod already provides an NPC class for '" + item.Id + "' - skipping the generated one (compiled wins). If you migrated this pack to auto-registration, remove the old consumer DLL."); } continue; } string text = "Personnel_" + item.Id; if (takenTypeNames.Contains(text)) { Instance log2 = Core.Log; if (log2 != null) { log2.Error($"auto-register: type name '{text}' collides with an existing NPC class - '{item.Id}' is NOT registered. Rename the NPC or its pack."); } continue; } try { if (SelfTest(EmitOne(text, item.Id), item.Id)) { _emittedIds.Add(item.Id); takenTypeNames.Add(text); num++; } } catch (Exception ex) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning($"auto-register: emitting '{item.Id}' failed ({ex.Message}) - skipped."); } } } if (num > 0) { Instance log4 = Core.Log; if (log4 != null) { log4.Msg($"auto-registered {num} pack NPC(s) as world NPCs (assembly '{"Personnel.Generated"}')."); } } return num; } private static Type EmitOne(string typeName, string defId) { if (_module == null) { _module = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Personnel.Generated"), AssemblyBuilderAccess.Run).DefineDynamicModule("Personnel.Generated"); } TypeBuilder typeBuilder = _module.DefineType("Personnel.Generated." + typeName, TypeAttributes.Public | TypeAttributes.Sealed, typeof(PersonnelNpc)); MethodBuilder methodBuilder = typeBuilder.DefineMethod("get_DefId", MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName, typeof(string), Type.EmptyTypes); ILGenerator iLGenerator = methodBuilder.GetILGenerator(); iLGenerator.Emit(OpCodes.Ldstr, defId); iLGenerator.Emit(OpCodes.Ret); typeBuilder.DefineProperty("DefId", PropertyAttributes.None, typeof(string), null).SetGetMethod(methodBuilder); typeBuilder.DefineMethodOverride(methodBuilder, BaseDefIdGetter()); typeBuilder.DefineDefaultConstructor(MethodAttributes.Public); return typeBuilder.CreateTypeInfo(); } private static bool SelfTest(Type t, string defId) { if (t == null || t.IsAbstract || !typeof(PersonnelNpc).IsAssignableFrom(t)) { Instance log = Core.Log; if (log != null) { log.Error("auto-register: emitted type for '" + defId + "' is malformed - skipped."); } return false; } object uninitializedObject = FormatterServices.GetUninitializedObject(t); string text = BaseDefIdGetter().Invoke(uninitializedObject, null) as string; if (!string.Equals(text, defId, StringComparison.Ordinal)) { Instance log2 = Core.Log; if (log2 != null) { log2.Error($"auto-register: emitted type for '{defId}' returned DefId '{text}' - skipped."); } return false; } return true; } private static MethodInfo BaseDefIdGetter() { return typeof(PersonnelNpc).GetProperty("DefId", BindingFlags.Instance | BindingFlags.NonPublic).GetGetMethod(nonPublic: true); } private static void CollectExistingConsumers(out HashSet takenDefIds, out HashSet takenTypeNames) { takenDefIds = new HashSet(StringComparer.OrdinalIgnoreCase); takenTypeNames = new HashSet(StringComparer.Ordinal); MethodInfo methodInfo = BaseDefIdGetter(); Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (assembly.IsDynamic && assembly.GetName().Name == "Personnel.Generated") { continue; } Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } catch { continue; } Type[] array = types; foreach (Type type in array) { if (type == null || type.IsAbstract) { continue; } try { if (!typeof(NPC).IsAssignableFrom(type)) { continue; } takenTypeNames.Add(type.Name); if (typeof(PersonnelNpc).IsAssignableFrom(type)) { object uninitializedObject = FormatterServices.GetUninitializedObject(type); if (methodInfo.Invoke(uninitializedObject, null) is string text && !string.IsNullOrWhiteSpace(text)) { takenDefIds.Add(text); } } } catch { } } } } } internal static class ScheduleSpecFactory { public static List Build(NpcDef def) { List list = new List(); if (def?.Schedule == null) { return list; } foreach (NpcScheduleAction item in def.Schedule) { if (item == null) { continue; } try { IScheduleActionSpec val = BuildOne(def, item); if (val != null) { list.Add(val); } } catch (Exception ex) { Warn(def, item, "failed to build (" + ex.Message + ")"); } } return list; } private static IScheduleActionSpec BuildOne(NpcDef def, NpcScheduleAction a) { //IL_0996: Unknown result type (might be due to invalid IL or missing references) //IL_099c: Expected O, but got Unknown //IL_03fe: Unknown result type (might be due to invalid IL or missing references) //IL_0403: Unknown result type (might be due to invalid IL or missing references) //IL_040f: Unknown result type (might be due to invalid IL or missing references) //IL_041b: Unknown result type (might be due to invalid IL or missing references) //IL_0428: Expected O, but got Unknown //IL_0335: Unknown result type (might be due to invalid IL or missing references) //IL_033a: Unknown result type (might be due to invalid IL or missing references) //IL_0346: Unknown result type (might be due to invalid IL or missing references) //IL_0354: Expected O, but got Unknown //IL_0620: Unknown result type (might be due to invalid IL or missing references) //IL_0625: Unknown result type (might be due to invalid IL or missing references) //IL_062c: Unknown result type (might be due to invalid IL or missing references) //IL_0636: Unknown result type (might be due to invalid IL or missing references) //IL_0642: Unknown result type (might be due to invalid IL or missing references) //IL_0650: Expected O, but got Unknown //IL_043e: Unknown result type (might be due to invalid IL or missing references) //IL_0443: Unknown result type (might be due to invalid IL or missing references) //IL_044f: Unknown result type (might be due to invalid IL or missing references) //IL_045b: Unknown result type (might be due to invalid IL or missing references) //IL_0468: Expected O, but got Unknown //IL_0287: Unknown result type (might be due to invalid IL or missing references) //IL_028c: Unknown result type (might be due to invalid IL or missing references) //IL_0298: Unknown result type (might be due to invalid IL or missing references) //IL_02a4: Unknown result type (might be due to invalid IL or missing references) //IL_02b2: Expected O, but got Unknown //IL_08bb: Unknown result type (might be due to invalid IL or missing references) //IL_08c0: Unknown result type (might be due to invalid IL or missing references) //IL_08cc: Unknown result type (might be due to invalid IL or missing references) //IL_08d8: Unknown result type (might be due to invalid IL or missing references) //IL_08e6: Expected O, but got Unknown //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Unknown result type (might be due to invalid IL or missing references) //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Expected O, but got Unknown //IL_0498: Unknown result type (might be due to invalid IL or missing references) //IL_049d: Unknown result type (might be due to invalid IL or missing references) //IL_04a4: Unknown result type (might be due to invalid IL or missing references) //IL_04ae: Unknown result type (might be due to invalid IL or missing references) //IL_04ba: Unknown result type (might be due to invalid IL or missing references) //IL_04c8: Expected O, but got Unknown //IL_071e: Unknown result type (might be due to invalid IL or missing references) //IL_0723: Unknown result type (might be due to invalid IL or missing references) //IL_072a: Unknown result type (might be due to invalid IL or missing references) //IL_0734: Unknown result type (might be due to invalid IL or missing references) //IL_0740: Unknown result type (might be due to invalid IL or missing references) //IL_074e: Expected O, but got Unknown //IL_092f: Unknown result type (might be due to invalid IL or missing references) //IL_095d: Unknown result type (might be due to invalid IL or missing references) //IL_081c: Unknown result type (might be due to invalid IL or missing references) //IL_0822: Invalid comparison between Unknown and I4 //IL_07e8: Unknown result type (might be due to invalid IL or missing references) //IL_085e: Unknown result type (might be due to invalid IL or missing references) //IL_05d7: Unknown result type (might be due to invalid IL or missing references) //IL_0599: Unknown result type (might be due to invalid IL or missing references) switch (Fold(a.Type)) { case "walkto": { if (!a.Position.HasValue) { return Fail(def, a, "needs 'position'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } WalkToSpec val5 = new WalkToSpec { Destination = a.Position.Value, StartTime = a.Time, Name = a.Name }; if (a.FaceDestination.HasValue) { val5.FaceDestinationDirection = a.FaceDestination.Value; } if (a.Within.HasValue) { val5.Within = a.Within.Value; } if (a.WarpIfSkipped.HasValue) { val5.WarpIfSkipped = a.WarpIfSkipped.Value; } return (IScheduleActionSpec)(object)val5; } case "stayinbuilding": { if (string.IsNullOrWhiteSpace(a.Building)) { return Fail(def, a, "needs 'building'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } StayInBuildingSpec val7 = new StayInBuildingSpec { BuildingName = a.Building, StartTime = a.Time, Name = a.Name }; if (a.DurationMinutes.HasValue) { val7.DurationMinutes = a.DurationMinutes.Value; } if (a.DoorIndex.HasValue) { val7.DoorIndex = a.DoorIndex.Value; } return (IScheduleActionSpec)(object)val7; } case "sit": { if (string.IsNullOrWhiteSpace(a.SeatSet) && string.IsNullOrWhiteSpace(a.SeatSetPath)) { return Fail(def, a, "needs 'seatSet' or 'seatSetPath'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } SitSpec val3 = new SitSpec { StartTime = a.Time, Name = a.Name }; if (!string.IsNullOrWhiteSpace(a.SeatSet)) { val3.SeatSetName = a.SeatSet; } if (!string.IsNullOrWhiteSpace(a.SeatSetPath)) { val3.SeatSetPath = a.SeatSetPath; } if (a.DurationMinutes.HasValue) { val3.DurationMinutes = a.DurationMinutes.Value; } if (a.WarpIfSkipped.HasValue) { val3.WarpIfSkipped = a.WarpIfSkipped.Value; } if (a.IncludeInactive.HasValue) { val3.IncludeInactiveSearch = a.IncludeInactive.Value; } return (IScheduleActionSpec)(object)val3; } case "usevendingmachine": if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } return (IScheduleActionSpec)new UseVendingMachineSpec { StartTime = a.Time, MachineGUID = a.MachineGuid, Name = a.Name }; case "useatm": if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } return (IScheduleActionSpec)new UseATMSpec { StartTime = a.Time, ATMGUID = a.AtmGuid, Name = a.Name }; case "useslotmachine": { if (!a.Position.HasValue) { return Fail(def, a, "needs 'position'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } UseSlotMachineSpec val6 = new UseSlotMachineSpec { MachinePosition = a.Position.Value, StartTime = a.Time, Name = a.Name }; if (a.Bet.HasValue) { val6.BetAmount = a.Bet.Value; } if (a.Spins.HasValue) { val6.SpinCount = a.Spins.Value; } if (a.EndTime >= 0) { val6.EndTime = a.EndTime; } if (a.TimeBetweenSpins.HasValue) { val6.TimeBetweenSpins = a.TimeBetweenSpins.Value; } if (a.MaxSearchDistance.HasValue) { val6.MaxSearchDistance = a.MaxSearchDistance.Value; } if (!string.IsNullOrWhiteSpace(a.Mode)) { if (Parse.TryParseEnum((Fold(a.Mode) == "single") ? "SingleSpin" : a.Mode, out GamblingSessionMode result3)) { val6.SessionMode = result3; } else { Warn(def, a, $"unknown mode '{a.Mode}' - using {val6.SessionMode}"); } } return (IScheduleActionSpec)(object)val6; } case "locationdialogue": { if (!a.Position.HasValue) { return Fail(def, a, "needs 'position'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } LocationDialogueSpec val2 = new LocationDialogueSpec { Destination = a.Position.Value, StartTime = a.Time, Name = a.Name }; if (a.FaceDestination.HasValue) { val2.FaceDestinationDirection = a.FaceDestination.Value; } if (a.Within.HasValue) { val2.Within = a.Within.Value; } if (a.WarpIfSkipped.HasValue) { val2.WarpIfSkipped = a.WarpIfSkipped.Value; } if (a.GreetingOverride.HasValue) { val2.GreetingOverrideToEnable = a.GreetingOverride.Value; } if (a.Choice.HasValue) { val2.ChoiceToEnable = a.Choice.Value; } return (IScheduleActionSpec)(object)val2; } case "locationaction": { if (!a.Position.HasValue) { return Fail(def, a, "needs 'position'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } LocationBasedActionSpec val4 = new LocationBasedActionSpec { Destination = a.Position.Value, StartTime = a.Time, Name = a.Name }; if (a.DurationMinutes.HasValue) { val4.DurationMinutes = a.DurationMinutes.Value; } if (a.FaceDestination.HasValue) { val4.FaceDestinationDirection = a.FaceDestination.Value; } if (a.Within.HasValue) { val4.Within = a.Within.Value; } if (a.WarpIfSkipped.HasValue) { val4.WarpIfSkipped = a.WarpIfSkipped.Value; } if (!string.IsNullOrWhiteSpace(a.Action)) { if (Parse.TryParseEnum(a.Action, out LocationArriveBehaviour result)) { val4.ArriveBehaviour = result; } else { Warn(def, a, "unknown action '" + a.Action + "' - using None"); } } if (!string.IsNullOrWhiteSpace(a.EquippablePath)) { if ((int)val4.ArriveBehaviour == 3) { val4.DrinkEquippablePath = a.EquippablePath; } else { val4.EquippableAssetPath = a.EquippablePath; } } if (!string.IsNullOrWhiteSpace(a.GraffitiRegion)) { if (Parse.TryParseEnum(a.GraffitiRegion, out Region result2)) { val4.GraffitiRegion = result2; } else { Warn(def, a, "unknown graffitiRegion '" + a.GraffitiRegion + "' - ignored"); } } return (IScheduleActionSpec)(object)val4; } case "drivetocarpark": { if (string.IsNullOrWhiteSpace(a.ParkingLot)) { return Fail(def, a, "needs 'parkingLot'"); } if (a.Time < 0) { return Fail(def, a, "needs 'time'"); } DriveToCarParkSpec val = new DriveToCarParkSpec { ParkingLotName = a.ParkingLot, StartTime = a.Time, Name = a.Name }; if (!string.IsNullOrWhiteSpace(a.Vehicle)) { val.VehicleName = a.Vehicle; } if (!string.IsNullOrWhiteSpace(a.CreateVehicleCode)) { val.VehicleCode = a.CreateVehicleCode; if (a.CreateVehiclePosition.HasValue) { val.VehicleSpawnPosition = a.CreateVehiclePosition.Value; } if (a.CreateVehicleRotationY.HasValue) { val.VehicleSpawnRotation = Quaternion.Euler(0f, a.CreateVehicleRotationY.Value, 0f); } } if (string.IsNullOrWhiteSpace(a.Vehicle) && string.IsNullOrWhiteSpace(a.CreateVehicleCode)) { return Fail(def, a, "needs 'vehicle' or 'createVehicle'"); } return (IScheduleActionSpec)(object)val; } case "dealsignal": return (IScheduleActionSpec)new EnsureDealSignalSpec(); case "handledeal": { Instance log = Core.Log; if (log != null) { log.Msg("'" + def.Id + "': schedule action 'handleDeal' is obsolete (deals are automatic) - ignored."); } return null; } default: return Fail(def, a, "unknown action type"); } } private static IScheduleActionSpec Fail(NpcDef def, NpcScheduleAction a, string reason) { Warn(def, a, reason); return null; } private static void Warn(NpcDef def, NpcScheduleAction a, string message) { Instance log = Core.Log; if (log != null) { log.Warning($"'{def.Id}' schedule action '{a.Type}': {message} - skipped/ignored."); } } private static string Fold(string s) { if (s == null) { return ""; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } } } namespace Personnel.Registration { internal static class CustomLayerRegistry { private static readonly Dictionary _resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); private static string SourceLayer(bool face) { if (!face) { return "Avatar/Layers/Tattoos/chest/Chest_Bird"; } return "Avatar/Layers/Tattoos/face/Face_Teardrop"; } public static string EnsureLayer(string npcSource, string npcId, string packDir, string file, Texture2D tex, bool face) { string text = ((!string.IsNullOrEmpty(file)) ? Path.GetFileNameWithoutExtension(file) : "tex"); string text2 = (face ? "Face" : "body"); string text3 = "Avatar/Layers/Tattoos/personnel/" + text2 + "/" + Sanitize(npcSource) + "_" + Sanitize(npcId) + "_" + Sanitize(text); if (_resolved.TryGetValue(text3, out var value)) { return value; } try { if ((Object)(object)tex == (Object)null) { if (string.IsNullOrWhiteSpace(file)) { Instance log = Core.Log; if (log != null) { log.Warning($"Custom layer for '{npcSource}/{npcId}': no texture or file."); } return null; } string text4 = (Path.IsPathRooted(file) ? file : Path.Combine(packDir ?? "", file)); if (!File.Exists(text4)) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning($"Custom layer for '{npcSource}/{npcId}': PNG not found at '{text4}'."); } return null; } tex = TextureUtils.LoadTextureFromFile(text4, (FilterMode)1, (TextureWrapMode)1); if ((Object)(object)tex == (Object)null) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning($"Custom layer for '{npcSource}/{npcId}': failed to load '{text4}'."); } return null; } } if (!AvatarLayerFactory.CreateAndRegisterAvatarLayer(SourceLayer(face), text3, npcId ?? text, tex)) { Instance log4 = Core.Log; if (log4 != null) { log4.Warning($"Custom layer for '{npcSource}/{npcId}': CreateAndRegisterAvatarLayer failed."); } return null; } _resolved[text3] = text3; Instance log5 = Core.Log; if (log5 != null) { log5.Msg($"Registered custom layer '{npcSource}/{npcId}' ({text}) -> {text3}"); } return text3; } catch (Exception ex) { Instance log6 = Core.Log; if (log6 != null) { log6.Warning($"Custom layer for '{npcSource}/{npcId}': registration error - {ex.Message}"); } return null; } } private static string Sanitize(string s) { if (string.IsNullOrEmpty(s)) { return "x"; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { stringBuilder.Append((char.IsLetterOrDigit(c) || c == '-' || c == '_') ? c : '_'); } return stringBuilder.ToString(); } } internal static class NpcRegistry { private static readonly List _all = new List(); private static readonly HashSet _keys = new HashSet(StringComparer.OrdinalIgnoreCase); public static IReadOnlyList AllDefs => _all; public static event Action OnReloaded; public static bool Add(NpcDef def) { if (def == null || string.IsNullOrWhiteSpace(def.Id)) { return false; } if (string.IsNullOrWhiteSpace(def.Source)) { def.Source = "API"; } if (!_keys.Add(def.Key)) { return false; } _all.Add(def); return true; } public static int AddRange(IEnumerable defs) { int num = 0; if (defs == null) { return 0; } foreach (NpcDef def in defs) { if (Add(def)) { num++; } } return num; } public static bool TryGet(string id, out NpcDef def) { def = null; if (string.IsNullOrWhiteSpace(id)) { return false; } foreach (NpcDef item in _all) { if (string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase)) { def = item; return true; } } return false; } public static int LoadPacks() { return AddRange(PackLoader.LoadAll()); } public static void Reload() { for (int num = _all.Count - 1; num >= 0; num--) { if (_all[num].PackDir != null) { _keys.Remove(_all[num].Key); _all.RemoveAt(num); } } LoadPacks(); try { NpcRegistry.OnReloaded?.Invoke(); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("OnReloaded handler threw: " + ex.Message); } } } } } namespace Personnel.Model { public sealed class NpcDef { public string Id; public string DisplayName; public string Source; public string PackDir; public NpcAppearance Appearance = new NpcAppearance(); public string SaveId; public NpcBehavior Behavior; public NpcSpawn Spawn; public NpcContact Contact; public NpcRelationships Relationships; public NpcCustomer Customer; public NpcDealer Dealer; public NpcInventory Inventory; public List Schedule; public IReadOnlyDictionary Extensions = new Dictionary(); public string Key => (Source ?? "?") + "/" + (Id ?? "?"); } public sealed class NpcAppearance { public float Gender; public float Height = 0.98f; public float Weight = 0.4f; public Color SkinColor = Color32.op_Implicit(new Color32((byte)150, (byte)120, (byte)95, byte.MaxValue)); public string HairPath = ""; public Color HairColor = Color.black; public float EyebrowScale = 1f; public float EyebrowThickness = 1f; public float EyebrowRestingHeight; public float EyebrowRestingAngle; public Color LeftEyeLidColor = Color32.op_Implicit(new Color32((byte)150, (byte)120, (byte)95, byte.MaxValue)); public Color RightEyeLidColor = Color32.op_Implicit(new Color32((byte)150, (byte)120, (byte)95, byte.MaxValue)); public float LeftEyeTop = 0.5f; public float LeftEyeBottom = 0.5f; public float RightEyeTop = 0.5f; public float RightEyeBottom = 0.5f; public string EyeballMaterial = "Default"; public Color EyeBallTint = Color.white; public float PupilDilation = 1f; public List FaceLayers = new List(); public List BodyLayers = new List(); public List Accessories = new List(); public Dictionary Distortion = new Dictionary(); } public sealed class BoneDistortion { public Vector3 Scale = Vector3.one; public bool Hide; } public sealed class NpcLayer { public string Path; public string File; public Texture2D Texture; public Color Tint = Color.white; } public sealed class NpcBehavior { public float Aggression; public float MaxHealth = 100f; public float Scale = 1f; public string Conversation = "none"; } public sealed class NpcSpawn { public Vector3? Position; public float? RotationY; public string Region = ""; public bool? Physical; public bool Auto; } public sealed class NpcContact { public bool? Visible; public bool? MapMarker; } public sealed class NpcRelationships { public float? Delta; public bool? Unlocked; public string UnlockType; public List Connections; } public sealed class NpcRange { public float Min; public float Max; } public sealed class NpcCustomer { public NpcRange Spending; public NpcRange OrdersPerWeek; public string PreferredOrderDay; public int? OrderTime; public string Standards; public bool? AllowDirectApproach; public bool? GuaranteeFirstSample; public NpcRange MutualRelationRequirement; public float? CallPoliceChance; public float? DependenceBase; public float? DependenceMultiplier; public Dictionary Affinities; public List PreferredProperties; } public sealed class NpcDealer { public string Type; public float? Cut; public float? SigningFee; public string Home; public string CompletedDealsVariable; public bool? AllowInsufficientQuality; public bool? AllowExcessQuality; } public sealed class NpcInventoryItem { public string Id; public int Quantity = 1; } public sealed class NpcInventory { public NpcRange Cash; public List Items; public bool? ClearEachNight; } public sealed class NpcScheduleAction { public string Type; public int Time = -1; public string Name; public Vector3? Position; public bool? FaceDestination; public float? Within; public bool? WarpIfSkipped; public int? DurationMinutes; public string Building; public int? DoorIndex; public string SeatSet; public string SeatSetPath; public bool? IncludeInactive; public string MachineGuid; public string AtmGuid; public int? Bet; public string Mode; public int EndTime = -1; public int? Spins; public float? TimeBetweenSpins; public float? MaxSearchDistance; public int? GreetingOverride; public int? Choice; public string Action; public string EquippablePath; public string GraffitiRegion; public string ParkingLot; public string Vehicle; public string CreateVehicleCode; public Vector3? CreateVehiclePosition; public float? CreateVehicleRotationY; } } namespace Personnel.Content { internal static class ExamplePack { private const string ExampleManifestJson = "{\n \"name\": \"Personnel Examples\",\n \"author\": \"DooDesch\",\n \"npcs\": [\n {\n \"name\": \"Pale\",\n \"appearance\": {\n \"gender\": 0.5,\n \"height\": 1.0,\n \"weight\": 0.4,\n \"skinColor\": \"#8899AA\",\n \"hairPath\": \"\",\n \"hairColor\": \"#101014\",\n \"eyeBallTint\": \"#FFFFFF\",\n \"pupilDilation\": 0.8\n },\n \"extensions\": {\n \"backrooms\": {\n \"archetype\": \"faceling\",\n \"tierMin\": 1, \"tierMax\": 5,\n \"biomes\": [\"L0\", \"L1\"],\n \"weight\": 14, \"maxAlive\": 3, \"hostile\": false\n }\n }\n },\n {\n \"name\": \"Ashen\",\n \"appearance\": {\n \"gender\": 0.2,\n \"height\": 1.1,\n \"weight\": 0.6,\n \"skinColor\": \"#4A4A50\",\n \"hairColor\": \"#000000\",\n \"eyeBallTint\": \"#FFCC66\"\n },\n \"extensions\": {\n \"backrooms\": {\n \"archetype\": \"wanderer_hollow\",\n \"tierMin\": 3, \"tierMax\": 5,\n \"weight\": 10, \"maxAlive\": 1, \"hostile\": true\n }\n }\n },\n {\n \"name\": \"Errand Eddie\",\n \"appearance\": {\n \"gender\": 0.0,\n \"height\": 1.0,\n \"weight\": 0.5,\n \"skinColor\": \"#C09070\",\n \"hairColor\": \"#3A2A1A\"\n },\n \"spawn\": {\n \"x\": -66.4, \"y\": -2.9, \"z\": 86.1,\n \"rotationY\": 145.0,\n \"region\": \"Westville\",\n \"physical\": true,\n \"auto\": false\n },\n \"contact\": { \"visible\": true, \"mapMarker\": true },\n \"relationships\": { \"delta\": 1.5, \"unlockType\": \"Recommendation\" },\n \"customer\": {\n \"spending\": { \"min\": 300, \"max\": 900 },\n \"ordersPerWeek\": { \"min\": 1, \"max\": 3 },\n \"preferredOrderDay\": \"Friday\",\n \"orderTime\": \"19:30\",\n \"standards\": \"Moderate\",\n \"affinities\": { \"marijuana\": 0.6 }\n },\n \"inventory\": { \"cash\": { \"min\": 20, \"max\": 120 }, \"clearEachNight\": true },\n \"schedule\": [\n { \"type\": \"walkTo\", \"time\": \"07:30\", \"position\": [-70.1, -2.9, 80.0] },\n { \"type\": \"stayInBuilding\", \"time\": \"09:00\", \"duration\": 240, \"building\": \"Thrifty Threads\" },\n { \"type\": \"walkTo\", \"time\": \"14:00\", \"position\": [-66.4, -2.9, 86.1] }\n ]\n }\n ]\n}\n"; public static void ExtractIfEnabled() { if (!Preferences.LoadExamplePack) { return; } string text = Path.Combine(PackLoader.PacksRoot, "Examples"); try { string path = Path.Combine(text, "manifest.json"); if (File.Exists(path)) { Instance log = Core.Log; if (log != null) { log.Msg("Example pack already present - leaving it untouched."); } return; } Directory.CreateDirectory(text); File.WriteAllText(path, "{\n \"name\": \"Personnel Examples\",\n \"author\": \"DooDesch\",\n \"npcs\": [\n {\n \"name\": \"Pale\",\n \"appearance\": {\n \"gender\": 0.5,\n \"height\": 1.0,\n \"weight\": 0.4,\n \"skinColor\": \"#8899AA\",\n \"hairPath\": \"\",\n \"hairColor\": \"#101014\",\n \"eyeBallTint\": \"#FFFFFF\",\n \"pupilDilation\": 0.8\n },\n \"extensions\": {\n \"backrooms\": {\n \"archetype\": \"faceling\",\n \"tierMin\": 1, \"tierMax\": 5,\n \"biomes\": [\"L0\", \"L1\"],\n \"weight\": 14, \"maxAlive\": 3, \"hostile\": false\n }\n }\n },\n {\n \"name\": \"Ashen\",\n \"appearance\": {\n \"gender\": 0.2,\n \"height\": 1.1,\n \"weight\": 0.6,\n \"skinColor\": \"#4A4A50\",\n \"hairColor\": \"#000000\",\n \"eyeBallTint\": \"#FFCC66\"\n },\n \"extensions\": {\n \"backrooms\": {\n \"archetype\": \"wanderer_hollow\",\n \"tierMin\": 3, \"tierMax\": 5,\n \"weight\": 10, \"maxAlive\": 1, \"hostile\": true\n }\n }\n },\n {\n \"name\": \"Errand Eddie\",\n \"appearance\": {\n \"gender\": 0.0,\n \"height\": 1.0,\n \"weight\": 0.5,\n \"skinColor\": \"#C09070\",\n \"hairColor\": \"#3A2A1A\"\n },\n \"spawn\": {\n \"x\": -66.4, \"y\": -2.9, \"z\": 86.1,\n \"rotationY\": 145.0,\n \"region\": \"Westville\",\n \"physical\": true,\n \"auto\": false\n },\n \"contact\": { \"visible\": true, \"mapMarker\": true },\n \"relationships\": { \"delta\": 1.5, \"unlockType\": \"Recommendation\" },\n \"customer\": {\n \"spending\": { \"min\": 300, \"max\": 900 },\n \"ordersPerWeek\": { \"min\": 1, \"max\": 3 },\n \"preferredOrderDay\": \"Friday\",\n \"orderTime\": \"19:30\",\n \"standards\": \"Moderate\",\n \"affinities\": { \"marijuana\": 0.6 }\n },\n \"inventory\": { \"cash\": { \"min\": 20, \"max\": 120 }, \"clearEachNight\": true },\n \"schedule\": [\n { \"type\": \"walkTo\", \"time\": \"07:30\", \"position\": [-70.1, -2.9, 80.0] },\n { \"type\": \"stayInBuilding\", \"time\": \"09:00\", \"duration\": 240, \"building\": \"Thrifty Threads\" },\n { \"type\": \"walkTo\", \"time\": \"14:00\", \"position\": [-66.4, -2.9, 86.1] }\n ]\n }\n ]\n}\n"); Instance log2 = Core.Log; if (log2 != null) { log2.Msg("Wrote example NPC pack -> " + text); } } catch (Exception ex) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning("Example pack write failed: " + ex.Message); } } } } public sealed class NpcPackManifest { public string name; public string author; public int? schemaVersion; public string packId; public bool? autoRegister; public List npcs; } public sealed class NpcEntry { public string id; public string name; public string saveId; public AppearanceJson appearance; public BehaviorJson behavior; public SpawnJson spawn; public ContactJson contact; public RelationshipsJson relationships; public CustomerJson customer; public DealerJson dealer; public InventoryJson inventory; public List schedule; public JObject extensions; } public sealed class AppearanceJson { public float? gender; public float? height; public float? weight; public string skinColor; public string hairPath; public string hairColor; public float? eyebrowScale; public float? eyebrowThickness; public float? eyebrowRestingHeight; public float? eyebrowRestingAngle; public string leftEyeLidColor; public string rightEyeLidColor; public EyeJson leftEye; public EyeJson rightEye; public string eyeballMaterial; public string eyeBallTint; public float? pupilDilation; public List faceLayers; public List bodyLayers; public List accessories; public Dictionary distortion; } public sealed class BoneDistortionJson { public float? scaleX; public float? scaleY; public float? scaleZ; public bool? hide; } public sealed class EyeJson { public float? top; public float? bottom; } public sealed class LayerJson { public string path; public string file; public string kind; public string tint; public string color; } public sealed class BehaviorJson { public float? aggression; public float? maxHealth; public float? scale; public string conversation; } public sealed class SpawnJson { public float? x; public float? y; public float? z; public float? rotationY; public string region; public bool? physical; public bool? auto; } public sealed class ContactJson { public bool? visible; public bool? mapMarker; } public sealed class RelationshipsJson { public float? delta; public bool? unlocked; public string unlockType; public List connections; } public sealed class MinMaxJson { public float? min; public float? max; } public sealed class CustomerJson { public MinMaxJson spending; public MinMaxJson ordersPerWeek; public string preferredOrderDay; public string orderTime; public string standards; public bool? allowDirectApproach; public bool? guaranteeFirstSample; public MinMaxJson mutualRelationRequirement; public float? callPoliceChance; public DependenceJson dependence; public Dictionary affinities; public List preferredProperties; } public sealed class DependenceJson { public float? @base; public float? multiplier; } public sealed class DealerJson { public string type; public float? cut; public float? signingFee; public string home; public string completedDealsVariable; public bool? allowInsufficientQuality; public bool? allowExcessQuality; } public sealed class InventoryJson { public MinMaxJson cash; public List items; public bool? clearEachNight; } public sealed class ScheduleActionJson { public string type; public string time; public string name; public float[] position; public bool? faceDestination; public float? within; public bool? warpIfSkipped; public int? duration; public string building; public int? doorIndex; public string seatSet; public string seatSetPath; public bool? includeInactive; public string machineGuid; public string atmGuid; public int? bet; public string mode; public string endTime; public int? spins; public float? timeBetweenSpins; public float? maxSearchDistance; public int? greetingOverride; public int? choice; public string action; public string equippablePath; public string graffitiRegion; public string parkingLot; public string vehicle; public CreateVehicleJson createVehicle; } public sealed class CreateVehicleJson { public string code; public float[] position; public float? rotationY; } internal static class PackLoader { public const int SchemaVersion = 2; public static string PacksRoot => Path.Combine(MelonEnvironment.UserDataDirectory, "Personnel", "Packs"); public static List LoadAll() { List list = new List(); string packsRoot = PacksRoot; try { Directory.CreateDirectory(packsRoot); WriteReadmeIfMissing(packsRoot); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("Could not prepare packs folder '" + packsRoot + "': " + ex.Message); } return list; } string[] directories = Directory.GetDirectories(packsRoot); Array.Sort(directories, (IComparer?)StringComparer.OrdinalIgnoreCase); string[] array = directories; foreach (string text in array) { string path = Path.Combine(text, "manifest.json"); if (!File.Exists(path)) { continue; } string name = new DirectoryInfo(text).Name; try { NpcPackManifest npcPackManifest = JsonConvert.DeserializeObject(File.ReadAllText(path)); if (npcPackManifest?.npcs == null) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("Pack '" + name + "': manifest has no 'npcs' array - skipped."); } continue; } if (npcPackManifest.schemaVersion.HasValue && npcPackManifest.schemaVersion.Value > 2) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning($"Pack '{name}': schemaVersion {npcPackManifest.schemaVersion} is newer than this Personnel understands ({2}) - unknown fields will be ignored. " + "Update Personnel."); } } int num = 0; List list2 = new List(); foreach (NpcEntry npc in npcPackManifest.npcs) { NpcDef npcDef = ToDef(name, text, npcPackManifest, npc); if (npcDef != null) { list.Add(npcDef); num++; list2.Add(npcDef.Id); } } Instance log4 = Core.Log; if (log4 != null) { log4.Msg($"Pack '{name}' ({npcPackManifest.name ?? "unnamed"}): {num} NPC(s) [{string.Join(", ", list2)}]."); } } catch (Exception ex2) { Instance log5 = Core.Log; if (log5 != null) { log5.Warning("Pack '" + name + "': failed to read manifest.json - " + ex2.Message); } } } return list; } private static NpcDef ToDef(string packName, string packDir, NpcPackManifest manifest, NpcEntry e) { if (e == null) { return null; } string text = ((!string.IsNullOrWhiteSpace(e.name)) ? e.name : e.id); if (string.IsNullOrWhiteSpace(text)) { Instance log = Core.Log; if (log != null) { log.Warning("Pack '" + packName + "': an NPC entry has no 'name' (or 'id') - skipped."); } return null; } string text2 = Ids.Make((!string.IsNullOrWhiteSpace(manifest?.packId)) ? manifest.packId : packName, text); string text3 = Ids.Normalize(e.id); string id = ((!string.IsNullOrEmpty(text3)) ? text3 : text2); if (!string.IsNullOrEmpty(text3) && !string.Equals(text3, text2, StringComparison.Ordinal)) { Instance log2 = Core.Log; if (log2 != null) { log2.Msg($"Pack '{packName}': NPC '{text}' uses its authored id '{text3}' (derived would be '{text2}')."); } } NpcDef npcDef = new NpcDef { Id = id, SaveId = (string.IsNullOrWhiteSpace(e.saveId) ? null : Ids.Normalize(e.saveId)), DisplayName = text, Source = packName, PackDir = packDir, Appearance = BuildAppearance(e.appearance), Behavior = BuildBehavior(e.behavior), Spawn = BuildSpawn(e.spawn, manifest, e), Contact = BuildContact(e.contact), Relationships = BuildRelationships(e.relationships), Customer = BuildCustomer(packName, text, e.customer), Dealer = BuildDealer(e.dealer), Inventory = BuildInventory(packName, text, e.inventory), Schedule = BuildSchedule(packName, text, e.schedule), Extensions = BuildExtensions(e.extensions) }; if (npcDef.Spawn == null && (e.spawn?.auto ?? (manifest?.autoRegister == true))) { npcDef.Spawn = new NpcSpawn { Auto = true }; } return npcDef; } private static NpcAppearance BuildAppearance(AppearanceJson a) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_021e: Unknown result type (might be due to invalid IL or missing references) //IL_0223: Unknown result type (might be due to invalid IL or missing references) //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_0300: Unknown result type (might be due to invalid IL or missing references) //IL_0305: Unknown result type (might be due to invalid IL or missing references) NpcAppearance npcAppearance = new NpcAppearance(); if (a == null) { return npcAppearance; } if (a.gender.HasValue) { npcAppearance.Gender = a.gender.Value; } if (a.height.HasValue) { npcAppearance.Height = a.height.Value; } if (a.weight.HasValue) { npcAppearance.Weight = a.weight.Value; } npcAppearance.SkinColor = ColorParse.Parse(a.skinColor, npcAppearance.SkinColor); if (a.hairPath != null) { npcAppearance.HairPath = a.hairPath; } npcAppearance.HairColor = ColorParse.Parse(a.hairColor, npcAppearance.HairColor); if (a.eyebrowScale.HasValue) { npcAppearance.EyebrowScale = a.eyebrowScale.Value; } if (a.eyebrowThickness.HasValue) { npcAppearance.EyebrowThickness = a.eyebrowThickness.Value; } if (a.eyebrowRestingHeight.HasValue) { npcAppearance.EyebrowRestingHeight = a.eyebrowRestingHeight.Value; } if (a.eyebrowRestingAngle.HasValue) { npcAppearance.EyebrowRestingAngle = a.eyebrowRestingAngle.Value; } npcAppearance.LeftEyeLidColor = ColorParse.Parse(a.leftEyeLidColor, npcAppearance.SkinColor); npcAppearance.RightEyeLidColor = ColorParse.Parse(a.rightEyeLidColor, npcAppearance.SkinColor); if (a.leftEye != null) { if (a.leftEye.top.HasValue) { npcAppearance.LeftEyeTop = a.leftEye.top.Value; } if (a.leftEye.bottom.HasValue) { npcAppearance.LeftEyeBottom = a.leftEye.bottom.Value; } } if (a.rightEye != null) { if (a.rightEye.top.HasValue) { npcAppearance.RightEyeTop = a.rightEye.top.Value; } if (a.rightEye.bottom.HasValue) { npcAppearance.RightEyeBottom = a.rightEye.bottom.Value; } } if (!string.IsNullOrWhiteSpace(a.eyeballMaterial)) { npcAppearance.EyeballMaterial = a.eyeballMaterial; } npcAppearance.EyeBallTint = ColorParse.Parse(a.eyeBallTint, npcAppearance.EyeBallTint); if (a.pupilDilation.HasValue) { npcAppearance.PupilDilation = a.pupilDilation.Value; } AppendLayers(a.faceLayers, npcAppearance.FaceLayers); AppendLayers(a.bodyLayers, npcAppearance.BodyLayers); AppendLayers(a.accessories, npcAppearance.Accessories); if (a.distortion != null) { foreach (KeyValuePair item in a.distortion) { if (item.Value != null) { npcAppearance.Distortion[item.Key] = new BoneDistortion { Scale = new Vector3(item.Value.scaleX ?? 1f, item.Value.scaleY ?? 1f, item.Value.scaleZ ?? 1f), Hide = (item.Value.hide == true) }; } } } return npcAppearance; } private static void AppendLayers(List src, List dst) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (src == null) { return; } foreach (LayerJson item in src) { if (item != null && (!string.IsNullOrWhiteSpace(item.path) || !string.IsNullOrWhiteSpace(item.file))) { dst.Add(new NpcLayer { Path = item.path, File = item.file, Tint = ColorParse.Parse(item.tint ?? item.color, Color.white) }); } } } private static NpcBehavior BuildBehavior(BehaviorJson b) { if (b == null) { return null; } NpcBehavior npcBehavior = new NpcBehavior(); if (b.aggression.HasValue) { npcBehavior.Aggression = b.aggression.Value; } if (b.maxHealth.HasValue) { npcBehavior.MaxHealth = b.maxHealth.Value; } if (b.scale.HasValue) { npcBehavior.Scale = b.scale.Value; } if (!string.IsNullOrWhiteSpace(b.conversation)) { npcBehavior.Conversation = b.conversation; } return npcBehavior; } private static NpcSpawn BuildSpawn(SpawnJson s, NpcPackManifest manifest, NpcEntry e) { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) if (s == null) { return null; } NpcSpawn npcSpawn = new NpcSpawn { Region = (s.region ?? ""), RotationY = s.rotationY, Physical = s.physical, Auto = (s.auto ?? (manifest?.autoRegister == true)) }; if (s.x.HasValue && s.y.HasValue && s.z.HasValue) { npcSpawn.Position = new Vector3(s.x.Value, s.y.Value, s.z.Value); } return npcSpawn; } private static NpcContact BuildContact(ContactJson c) { if (c == null) { return null; } return new NpcContact { Visible = c.visible, MapMarker = c.mapMarker }; } private static NpcRelationships BuildRelationships(RelationshipsJson r) { if (r == null) { return null; } NpcRelationships npcRelationships = new NpcRelationships { Delta = r.delta, Unlocked = r.unlocked, UnlockType = (string.IsNullOrWhiteSpace(r.unlockType) ? null : r.unlockType) }; if (r.connections != null) { npcRelationships.Connections = new List(); foreach (string connection in r.connections) { if (!string.IsNullOrWhiteSpace(connection)) { npcRelationships.Connections.Add(connection.Trim()); } } } return npcRelationships; } private static NpcRange BuildRange(MinMaxJson m) { if (m == null || (!m.min.HasValue && !m.max.HasValue)) { return null; } float num = m.min ?? m.max.GetValueOrDefault(); float num2 = m.max ?? m.min.GetValueOrDefault(); if (num2 < num) { float num3 = num2; num2 = num; num = num3; } return new NpcRange { Min = num, Max = num2 }; } private static NpcCustomer BuildCustomer(string packName, string npcName, CustomerJson c) { if (c == null) { return null; } NpcCustomer npcCustomer = new NpcCustomer { Spending = BuildRange(c.spending), OrdersPerWeek = BuildRange(c.ordersPerWeek), PreferredOrderDay = c.preferredOrderDay, Standards = c.standards, AllowDirectApproach = c.allowDirectApproach, GuaranteeFirstSample = c.guaranteeFirstSample, MutualRelationRequirement = BuildRange(c.mutualRelationRequirement), CallPoliceChance = c.callPoliceChance, DependenceBase = c.dependence?.@base, DependenceMultiplier = c.dependence?.multiplier }; if (!string.IsNullOrWhiteSpace(c.orderTime)) { if (Parse.TryParseTime(c.orderTime, out var hhmm)) { npcCustomer.OrderTime = hhmm; } else { Instance log = Core.Log; if (log != null) { log.Warning($"Pack '{packName}': NPC '{npcName}' customer.orderTime '{c.orderTime}' is not a valid time - ignored."); } } } if (c.affinities != null && c.affinities.Count > 0) { npcCustomer.Affinities = new Dictionary(c.affinities); } if (c.preferredProperties != null) { npcCustomer.PreferredProperties = new List(); foreach (string preferredProperty in c.preferredProperties) { if (!string.IsNullOrWhiteSpace(preferredProperty)) { npcCustomer.PreferredProperties.Add(preferredProperty.Trim()); } } } return npcCustomer; } private static NpcDealer BuildDealer(DealerJson d) { if (d == null) { return null; } return new NpcDealer { Type = d.type, Cut = d.cut, SigningFee = d.signingFee, Home = d.home, CompletedDealsVariable = d.completedDealsVariable, AllowInsufficientQuality = d.allowInsufficientQuality, AllowExcessQuality = d.allowExcessQuality }; } private static NpcInventory BuildInventory(string packName, string npcName, InventoryJson inv) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Invalid comparison between Unknown and I4 //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Invalid comparison between Unknown and I4 if (inv == null) { return null; } NpcInventory npcInventory = new NpcInventory { Cash = BuildRange(inv.cash), ClearEachNight = inv.clearEachNight }; if (inv.items != null) { npcInventory.Items = new List(); foreach (JToken item in inv.items) { if (item == null) { continue; } try { if ((int)item.Type == 8) { string text = Extensions.Value((IEnumerable)item); if (!string.IsNullOrWhiteSpace(text)) { npcInventory.Items.Add(new NpcInventoryItem { Id = text.Trim() }); } } else if ((int)item.Type == 1) { string text2 = item.Value((object)"id"); int num = item.Value((object)"quantity") ?? 1; if (!string.IsNullOrWhiteSpace(text2)) { npcInventory.Items.Add(new NpcInventoryItem { Id = text2.Trim(), Quantity = ((num < 1) ? 1 : num) }); } } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning($"Pack '{packName}': NPC '{npcName}' has a malformed inventory item ({ex.Message}) - skipped."); } } } } return npcInventory; } private static List BuildSchedule(string packName, string npcName, List actions) { //IL_0390: Unknown result type (might be due to invalid IL or missing references) //IL_0440: Unknown result type (might be due to invalid IL or missing references) if (actions == null || actions.Count == 0) { return null; } List list = new List(); foreach (ScheduleActionJson action in actions) { if (action == null) { continue; } if (string.IsNullOrWhiteSpace(action.type)) { Instance log = Core.Log; if (log != null) { log.Warning($"Pack '{packName}': NPC '{npcName}' has a schedule action without 'type' - skipped."); } continue; } NpcScheduleAction npcScheduleAction = new NpcScheduleAction { Type = action.type.Trim(), Name = action.name, FaceDestination = action.faceDestination, Within = action.within, WarpIfSkipped = action.warpIfSkipped, DurationMinutes = action.duration, Building = action.building, DoorIndex = action.doorIndex, SeatSet = action.seatSet, SeatSetPath = action.seatSetPath, IncludeInactive = action.includeInactive, MachineGuid = action.machineGuid, AtmGuid = action.atmGuid, Bet = action.bet, Mode = action.mode, Spins = action.spins, TimeBetweenSpins = action.timeBetweenSpins, MaxSearchDistance = action.maxSearchDistance, GreetingOverride = action.greetingOverride, Choice = action.choice, Action = action.action, EquippablePath = action.equippablePath, GraffitiRegion = action.graffitiRegion, ParkingLot = action.parkingLot, Vehicle = action.vehicle, CreateVehicleCode = action.createVehicle?.code, CreateVehicleRotationY = action.createVehicle?.rotationY }; if (!string.IsNullOrWhiteSpace(action.time)) { if (!Parse.TryParseTime(action.time, out var hhmm)) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning($"Pack '{packName}': NPC '{npcName}' schedule action '{action.type}' has invalid time '{action.time}' - action skipped."); } continue; } npcScheduleAction.Time = hhmm; } if (!string.IsNullOrWhiteSpace(action.endTime)) { if (Parse.TryParseTime(action.endTime, out var hhmm2)) { npcScheduleAction.EndTime = hhmm2; } else { Instance log3 = Core.Log; if (log3 != null) { log3.Warning($"Pack '{packName}': NPC '{npcName}' schedule action '{action.type}' has invalid endTime '{action.endTime}' - ignored."); } } } if (action.position != null) { if (!Parse.TryParseVec3(action.position, out var v)) { Instance log4 = Core.Log; if (log4 != null) { log4.Warning($"Pack '{packName}': NPC '{npcName}' schedule action '{action.type}' has an invalid position (expected [x,y,z]) - action skipped."); } continue; } npcScheduleAction.Position = v; } if (action.createVehicle?.position != null) { if (Parse.TryParseVec3(action.createVehicle.position, out var v2)) { npcScheduleAction.CreateVehiclePosition = v2; } else { Instance log5 = Core.Log; if (log5 != null) { log5.Warning($"Pack '{packName}': NPC '{npcName}' schedule action '{action.type}' has an invalid createVehicle.position - ignored."); } } } list.Add(npcScheduleAction); } if (list.Count <= 0) { return null; } return list; } private static IReadOnlyDictionary BuildExtensions(JObject ext) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (ext == null) { return dictionary; } foreach (JProperty item in ext.Properties()) { try { dictionary[item.Name] = item.Value.ToString((Formatting)0); } catch { } } return dictionary; } private static void WriteReadmeIfMissing(string root) { string path = Path.Combine(root, "README.txt"); if (!File.Exists(path)) { File.WriteAllText(path, "Personnel - custom NPC packs\n============================\n\nDrop one folder per pack in this directory. Each pack needs a manifest.json and (optionally) the PNG files\nits custom layers reference. Build packs visually with the \"Personify\" editor (Side Hustle gamemode).\n\nFolder layout:\n Packs/\n MyPack/\n manifest.json\n grin.png (optional custom layer texture)\n\nmanifest.json:\n{\n \"name\": \"My Pack\",\n \"author\": \"you\",\n \"npcs\": [\n {\n \"id\": \"faceling_pale\",\n \"name\": \"Pale Faceling\",\n \"appearance\": {\n \"gender\": 0.5, \"height\": 1.0, \"weight\": 0.4, \"skinColor\": \"#8899AA\",\n \"hairPath\": \"\", \"hairColor\": \"#000000\",\n \"eyeBallTint\": \"#FFFFFF\", \"pupilDilation\": 1,\n \"faceLayers\": [ { \"file\": \"grin.png\", \"kind\": \"face\", \"tint\": \"#FFFFFF\" } ],\n \"bodyLayers\": [ { \"path\": \"Avatar/Layers/Body/...\", \"tint\": \"#334455\" } ],\n \"accessories\": [ ]\n },\n \"extensions\": {\n \"backrooms\": { \"archetype\": \"faceling\", \"tierMin\": 1, \"tierMax\": 5,\n \"biomes\": [\"L0\",\"L1\"], \"weight\": 14, \"maxAlive\": 3, \"hostile\": false }\n }\n }\n ]\n}\n\nLayers: use \"path\" to reference an existing in-game layer, OR \"file\" (a PNG next to manifest.json)\n with \"kind\": face|body for a custom layer. Custom PNGs must match the game's body/face UV.\nextensions: free-form per-consumer data. The Backrooms mod reads the \"backrooms\" block; other mods\n ignore it.\n\nSince Personnel 2.0 an NPC entry can also carry world data - no mod code needed:\n \"spawn\": { \"x\": 0, \"y\": 0, \"z\": 0, \"rotationY\": 0, \"region\": \"Westville\",\n \"physical\": true, \"auto\": true }\n \"contact\": { \"visible\": true, \"mapMarker\": true }\n \"relationships\": { \"delta\": 1.5, \"unlockType\": \"Recommendation\", \"connections\": [\"other_npc_id\"] }\n \"customer\": spending/ordersPerWeek/preferredOrderDay/orderTime/standards/affinities/...\n \"dealer\": type/cut/signingFee/home/...\n \"inventory\": { \"cash\": { \"min\": 20, \"max\": 120 }, \"items\": [\"baggie\"], \"clearEachNight\": true }\n \"schedule\": [ { \"type\": \"walkTo\", \"time\": \"07:30\", \"position\": [x, y, z] },\n { \"type\": \"stayInBuilding\", \"time\": \"09:00\", \"duration\": 240, \"building\": \"...\" } ]\nSet \"autoRegister\": true at the top level (or \"auto\": true per NPC) and Personnel spawns them as real,\nnetworked, saved world NPCs. Times are \"HH:MM\". Full reference:\nhttps://github.com/DooDesch-Mods/ScheduleOne-Personnel/wiki/Pack-Format\n"); } } } } namespace Personnel.Config { internal static class Preferences { private const string CategoryId = "Personnel_01_Main"; private static MelonPreferences_Category _category; private static MelonPreferences_Entry _loadExamplePack; private static MelonPreferences_Entry _enableAutoRegister; internal static bool LoadExamplePack => _loadExamplePack?.Value ?? false; internal static bool EnableAutoRegister => _enableAutoRegister?.Value ?? true; internal static void Initialize() { if (_category == null) { _category = MelonPreferences.CreateCategory("Personnel_01_Main", "Personnel (Custom NPCs)"); _loadExamplePack = _category.CreateEntry("LoadExamplePack", false, "Load example NPC pack", "OFF by default. When ON, Personnel drops a small example pack into UserData/Personnel/Packs/Examples on startup (if not already there) so you get a working manifest template to copy for your own pack. Requires a game restart.", false, false, (ValueValidator)null, (string)null); _enableAutoRegister = _category.CreateEntry("EnableAutoRegister", true, "Auto-register pack NPCs", "ON by default. Packs that opt in (\"autoRegister\": true, or per-NPC \"spawn\": { \"auto\": true }) get their NPCs registered as real world NPCs without any consumer mod. Turn OFF as a kill switch if a pack misbehaves. Requires a game restart.", false, false, (ValueValidator)null, (string)null); } } } } namespace Personnel.Appearance { public static class AvatarSettingsFactory { public static AvatarSettings BuildAvatarSettings(NpcDef def) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) if (def == null) { return null; } NpcAppearance npcAppearance = def.Appearance ?? new NpcAppearance(); AvatarSettings val = ScriptableObject.CreateInstance(); ((Object)val).hideFlags = (HideFlags)32; val.Gender = npcAppearance.Gender; val.Height = npcAppearance.Height; val.Weight = npcAppearance.Weight; val.SkinColor = npcAppearance.SkinColor; val.HairPath = npcAppearance.HairPath ?? string.Empty; val.HairColor = npcAppearance.HairColor; val.EyebrowScale = npcAppearance.EyebrowScale; val.EyebrowThickness = npcAppearance.EyebrowThickness; val.EyebrowRestingHeight = npcAppearance.EyebrowRestingHeight; val.EyebrowRestingAngle = npcAppearance.EyebrowRestingAngle; val.LeftEyeLidColor = npcAppearance.LeftEyeLidColor; val.RightEyeLidColor = npcAppearance.RightEyeLidColor; val.LeftEyeRestingState = new EyeLidConfiguration { topLidOpen = npcAppearance.LeftEyeTop, bottomLidOpen = npcAppearance.LeftEyeBottom }; val.RightEyeRestingState = new EyeLidConfiguration { topLidOpen = npcAppearance.RightEyeTop, bottomLidOpen = npcAppearance.RightEyeBottom }; val.EyeballMaterialIdentifier = (string.IsNullOrEmpty(npcAppearance.EyeballMaterial) ? "Default" : npcAppearance.EyeballMaterial); val.EyeBallTint = npcAppearance.EyeBallTint; val.PupilDilation = npcAppearance.PupilDilation; List val2 = new List(); List val3 = new List(); List val4 = new List(); AddLayers(def, npcAppearance.FaceLayers, val2, face: true); AddLayers(def, npcAppearance.BodyLayers, val3, face: false); AddAccessories(npcAppearance.Accessories, val4); AvatarLayerSlots.OrderFaceLayers(val2); foreach (LayerSetting item in AvatarLayerSlots.TrimFaceToBudget(val2, BudgetPriority)) { Instance log = Core.Log; if (log != null) { log.Warning("[appearance] '" + def.Id + "' exceeds the face layer budget, dropped " + item.layerPath); } } foreach (LayerSetting item2 in AvatarLayerSlots.TrimToBudget(val3, BudgetPriority)) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[appearance] '" + def.Id + "' exceeds the body layer budget, dropped " + item2.layerPath); } } val.FaceLayerSettings = val2; val.BodyLayerSettings = val3; val.AccessorySettings = val4; return val; } private static void AddLayers(NpcDef def, List layers, List dst, bool face) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown //IL_0038: Unknown result type (might be due to invalid IL or missing references) if (layers == null) { return; } foreach (NpcLayer layer in layers) { if (layer != null) { string text = ResolvePath(def, layer, face); if (!string.IsNullOrEmpty(text)) { LayerSetting val = new LayerSetting(); val.layerPath = text; val.layerTint = layer.Tint; dst.Add(val); } } } } private static int BudgetPriority(string path) { if (string.IsNullOrEmpty(path)) { return 0; } if (path.IndexOf("personnel", StringComparison.OrdinalIgnoreCase) >= 0) { return 3; } if (path.IndexOf("/Tattoos/", StringComparison.OrdinalIgnoreCase) >= 0) { return 2; } return 1; } private static void AddAccessories(List layers, List dst) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (layers == null) { return; } foreach (NpcLayer layer in layers) { if (layer != null && !string.IsNullOrWhiteSpace(layer.Path)) { AccessorySetting val = new AccessorySetting(); val.path = layer.Path; val.color = layer.Tint; dst.Add(val); } } } public static void ApplyToDefaults(AvatarDefaultsBuilder ab, NpcDef def) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Unknown result type (might be due to invalid IL or missing references) if (ab == null || def == null) { return; } NpcAppearance npcAppearance = def.Appearance ?? new NpcAppearance(); ab.Gender = npcAppearance.Gender; ab.Height = npcAppearance.Height; ab.Weight = npcAppearance.Weight; ab.SkinColor = Color32.op_Implicit(npcAppearance.SkinColor); ab.HairPath = npcAppearance.HairPath ?? string.Empty; ab.HairColor = npcAppearance.HairColor; ab.EyebrowScale = npcAppearance.EyebrowScale; ab.EyebrowThickness = npcAppearance.EyebrowThickness; ab.EyebrowRestingHeight = npcAppearance.EyebrowRestingHeight; ab.EyebrowRestingAngle = npcAppearance.EyebrowRestingAngle; ab.LeftEyeLidColor = npcAppearance.LeftEyeLidColor; ab.RightEyeLidColor = npcAppearance.RightEyeLidColor; ab.LeftEye = (npcAppearance.LeftEyeTop, npcAppearance.LeftEyeBottom); ab.RightEye = (npcAppearance.RightEyeTop, npcAppearance.RightEyeBottom); ab.EyeballMaterialIdentifier = (string.IsNullOrEmpty(npcAppearance.EyeballMaterial) ? "Default" : npcAppearance.EyeballMaterial); ab.EyeBallTint = npcAppearance.EyeBallTint; ab.PupilDilation = npcAppearance.PupilDilation; foreach (var (text, val) in OrderedFaceLayers(def, npcAppearance.FaceLayers)) { ab.WithFaceLayer(text, val); } List<(string, Color)> list = new List<(string, Color)>(); foreach (NpcLayer item in npcAppearance.BodyLayers ?? new List()) { if (item != null) { string text2 = ResolvePath(def, item, face: false); if (!string.IsNullOrEmpty(text2)) { list.Add((text2, item.Tint)); } } } TrimByRank(list, 8, def.Id, "body"); foreach (var (text3, val2) in list) { ab.WithBodyLayer(text3, val2); } foreach (NpcLayer accessory in npcAppearance.Accessories) { if (accessory != null && !string.IsNullOrWhiteSpace(accessory.Path)) { ab.WithAccessoryLayer(accessory.Path, accessory.Tint); } } } private static List<(string path, Color tint)> OrderedFaceLayers(NpcDef def, List layers) { //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) List<(string, Color)> list = new List<(string, Color)>(); List<(string, Color)> list2 = new List<(string, Color)>(); List<(string, Color)> list3 = new List<(string, Color)>(); foreach (NpcLayer item in layers ?? new List()) { if (item == null) { continue; } string text = ResolvePath(def, item, face: true); if (!string.IsNullOrEmpty(text) && !(text == "Avatar/Layers/None")) { if (list.Count == 0 && AvatarLayerSlots.IsMouthLayer(text)) { list.Add((text, item.Tint)); } else if (list2.Count == 0 && AvatarLayerSlots.IsFacialHairLayer(text)) { list2.Add((text, item.Tint)); } else { list3.Add((text, item.Tint)); } } } TrimByRank(list3, 3, def.Id, "face"); List<(string, Color)> list4 = new List<(string, Color)>(); list4.Add((list.Count > 0) ? list[0] : ("Avatar/Layers/None", Color.white)); list4.Add((list2.Count > 0) ? list2[0] : ("Avatar/Layers/None", Color.white)); list4.AddRange(list3); return list4; } private static void TrimByRank(List<(string path, Color tint)> layers, int max, string id, string what) { while (layers.Count > max) { int index = 0; for (int i = 1; i < layers.Count; i++) { if (BudgetPriority(layers[i].path) < BudgetPriority(layers[index].path)) { index = i; } } Instance log = Core.Log; if (log != null) { log.Warning("[appearance] '" + id + "' exceeds the " + what + " layer budget, dropped " + layers[index].path); } layers.RemoveAt(index); } } internal static string ResolvePath(NpcDef def, NpcLayer l, bool face) { if ((Object)(object)l.Texture != (Object)null) { return CustomLayerRegistry.EnsureLayer(def.Source, def.Id, def.PackDir, null, l.Texture, face); } if (!string.IsNullOrWhiteSpace(l.File)) { return CustomLayerRegistry.EnsureLayer(def.Source, def.Id, def.PackDir, l.File, null, face); } return l.Path; } } }