using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("HowToFish.ExpansionKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Unofficial, game-asset-free content and layout contracts for How to Fish expansions.")] [assembly: AssemblyFileVersion("0.7.2.0")] [assembly: AssemblyInformationalVersion("0.7.2")] [assembly: AssemblyProduct("HowToFish.ExpansionKit")] [assembly: AssemblyTitle("HowToFish.ExpansionKit")] [assembly: AssemblyVersion("0.7.2.0")] [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 HowToFish.ExpansionKit { public enum ItemKind { Fish, Rod, Weapon, Boss, Trophy } public enum NpcRole { Merchant, Quest, Dealer } public enum ShopKind { Item, Lure, Attachment, Ammunition, BoatRadar, NativeItem, NativeLure, Sharpening, Pocket, Motor } public enum PlacementKind { Player, Boat, Npc, Shop, Service, Fishing, Landmark, Table, TablePlayer } public enum RewardKind { Lure, Coordinates } public enum SupportKind { Inferred, None, Ground, Surface, Water } public sealed class ExpansionDefinition { public const int CurrentSchema = 2; public int SchemaVersion { get; set; } = 2; public string Key { get; set; } = ""; public string Title { get; set; } = ""; public string GameBuild { get; set; } = ""; public string SceneBundle { get; set; } = ""; public string SceneName { get; set; } = ""; public int IslandId { get; set; } public int NativeUnlockCount { get; set; } public int NextIslandId { get; set; } public int ContentCollectionId { get; set; } public int EncounterCollectionId { get; set; } public List Items { get; set; } = new List(); public List Lures { get; set; } = new List(); public List Npcs { get; set; } = new List(); public List Shops { get; set; } = new List(); public List Placements { get; set; } = new List(); public List Encounters { get; set; } = new List(); public List Quests { get; set; } = new List(); public IEnumerable AssetKeys => Items.Select((ItemDefinition item) => item.AssetKey).Concat(Lures.Select((LureDefinition lure) => lure.AssetKey)).Distinct(StringComparer.Ordinal); public void Validate() { IReadOnlyList readOnlyList = Inspect(); if (readOnlyList.Count != 0) { throw new InvalidDataException(string.Join(Environment.NewLine, readOnlyList)); } } public IReadOnlyList Inspect() { List errors = new List(); Require(SchemaVersion == 2, "Unsupported expansion schema."); Require(ValidKey(Key) && !string.IsNullOrWhiteSpace(Title), "The expansion needs a key and title."); Require(Regex.IsMatch(GameBuild ?? "", "^[0-9]+$"), "GameBuild must pin a native build number."); Require(ValidFile(SceneBundle) && ValidSceneName(SceneName), "Scene bundle and scene names must be safe local names."); Require(IslandId >= 5 && IslandId <= 254, "IslandId must be 5..254; 255 is the native unload sentinel."); Require(NativeUnlockCount >= 1 && NativeUnlockCount <= 5, "NativeUnlockCount must be between one and five."); Require(NextIslandId >= 0 && NextIslandId <= 255 && NextIslandId != IslandId, "The next-island identifier is invalid."); Require(ContentCollectionId >= 1 && ContentCollectionId <= 65535 && EncounterCollectionId >= 1 && EncounterCollectionId <= 65535 && ContentCollectionId != EncounterCollectionId, "Content and encounter collections must be distinct nonzero ushort identifiers."); if (Items == null || Lures == null || Npcs == null || Shops == null || Placements == null || Encounters == null || Quests == null) { errors.Add("All definition collections are required, even when empty."); return errors; } Require(Items.Count > 0 && Items.Count <= 128 && Lures.Count <= 64 && Npcs.Count <= 32 && Shops.Count <= 128 && Placements.Count <= 256 && Encounters.Count <= 32 && Quests.Count <= 32, "The definition exceeds its bounded content budget or has no items."); CheckKeys(Items, (ItemDefinition item) => item.Key, "item", errors); CheckKeys(Lures, (LureDefinition lure) => lure.Key, "lure", errors); CheckKeys(Npcs, (NpcDefinition npcDefinition) => npcDefinition.Key, "NPC", errors); CheckKeys(Shops, (ShopDefinition shopDefinition) => shopDefinition.Key, "shop", errors); CheckKeys(Encounters, (EncounterDefinition encounterDefinition) => encounterDefinition.Key, "encounter", errors); CheckKeys(Quests, (QuestStageDefinition questStageDefinition) => questStageDefinition.Key, "quest", errors); if (Items.Any((ItemDefinition item) => item == null) || Lures.Any((LureDefinition lure) => lure == null) || Npcs.Any((NpcDefinition npcDefinition) => npcDefinition == null) || Shops.Any((ShopDefinition shopDefinition) => shopDefinition == null) || Placements.Any((PlacementDefinition placementDefinition) => placementDefinition == null) || Encounters.Any((EncounterDefinition encounterDefinition) => encounterDefinition == null) || Quests.Any((QuestStageDefinition questStageDefinition) => questStageDefinition == null)) { errors.Add("Content collections cannot contain null entries."); return errors; } Require(Items.Select((ItemDefinition item) => item.Id).Distinct().Count() == Items.Count, "Item identifiers must be unique."); Require(Npcs.Select((NpcDefinition npcDefinition) => npcDefinition.Id).Distinct().Count() == Npcs.Count, "NPC identifiers must be unique."); Require(Placements.Select((PlacementDefinition placementDefinition) => placementDefinition.Marker).Distinct(StringComparer.Ordinal).Count() == Placements.Count, "Placement marker names must be unique."); foreach (ItemDefinition item in Items) { Require(item.Id >= 0 && item.Id <= 255 && Enum.IsDefined(typeof(ItemKind), item.Kind) && ValidKey(item.AssetKey) && !string.IsNullOrWhiteSpace(item.Title) && item.Worth >= 0 && item.Cost >= 0 && Finite(item.CatchWeight) && item.CatchWeight >= 0f, "Invalid item fields: " + item.Key); Require(item.NativeDonorId == -1 || (item.NativeDonorId >= 0 && item.NativeDonorId <= 85), "Invalid native item donor: " + item.Key); Require(item.Kind != ItemKind.Fish || item.Health > 0, "A fish needs positive health: " + item.Key); } foreach (LureDefinition lure in Lures) { Require(ValidKey(lure.AssetKey) && !string.IsNullOrWhiteSpace(lure.Title) && !string.IsNullOrWhiteSpace(lure.Description) && lure.Cost >= 0 && Finite(lure.LossPercent) && lure.LossPercent >= 0f && lure.LossPercent <= 100f && Finite(lure.CatchTimeMin) && Finite(lure.CatchTimeMax) && lure.CatchTimeMin > 0f && lure.CatchTimeMax >= lure.CatchTimeMin, "Invalid lure fields: " + lure.Key); if (lure.Catches == null || lure.Catches.Count == 0 || lure.Catches.Count > 128 || lure.Catches.Any((CatchDefinition catchEntry) => catchEntry == null)) { errors.Add("The lure needs a nonempty catch table: " + lure.Key); continue; } Require(lure.Catches.Select((CatchDefinition catchDefinition) => catchDefinition.Item).Distinct(StringComparer.Ordinal).Count() == lure.Catches.Count, "A lure repeats a catch-table entry: " + lure.Key); Require(((IEnumerable)lure.Catches).Sum((Func)((CatchDefinition catchDefinition) => catchDefinition.Weight)) <= 3.4028234663852886E+38, "A lure's combined weight exceeds native floating-point capacity: " + lure.Key); foreach (CatchDefinition entry in lure.Catches) { Require(Finite(entry.Weight) && entry.Weight > 0f && Items.Any((ItemDefinition item) => item.Key == entry.Item && (item.Kind == ItemKind.Fish || item.Kind == ItemKind.Boss)), "Invalid lure catch reference: " + lure.Key + " -> " + entry.Item); } } foreach (NpcDefinition npc2 in Npcs) { Require(npc2.Id >= 0 && npc2.Id <= 255 && !string.IsNullOrWhiteSpace(npc2.Title) && Enum.IsDefined(typeof(NpcRole), npc2.Role), "Invalid NPC fields: " + npc2.Key); } foreach (ShopDefinition shop in Shops) { Require(Enum.IsDefined(typeof(ShopKind), shop.Kind), "Unknown shop kind: " + shop.Key); Require((shop.Kind != ShopKind.NativeItem && shop.Kind != ShopKind.NativeLure) ? (shop.NativeId == -1 && shop.NativeName == "") : (shop.NativeId >= ((shop.Kind == ShopKind.NativeLure) ? 1 : 0) && shop.NativeId <= 255 && !string.IsNullOrWhiteSpace(shop.NativeName) && shop.NativeName.Length <= 120 && shop.NativeName == shop.NativeName.Trim() && !shop.NativeName.Any(char.IsControl)), "Native shop identity fields are missing or attached to a non-native shop: " + shop.Key); bool condition; switch (shop.Kind) { case ShopKind.Item: condition = Items.Any((ItemDefinition item) => item.Key == shop.Reference && item.Kind != ItemKind.Boss && item.Kind != ItemKind.Trophy); break; case ShopKind.Lure: condition = Lures.Any((LureDefinition lure) => lure.Key == shop.Reference && lure.Cost > 0); break; case ShopKind.Attachment: case ShopKind.Ammunition: condition = ValidKey(shop.Reference); break; case ShopKind.BoatRadar: condition = shop.Reference == "boat_radar"; break; case ShopKind.NativeItem: condition = ValidKey(shop.Reference) && !Items.Any((ItemDefinition item) => item.Id == shop.NativeId); break; case ShopKind.NativeLure: condition = ValidKey(shop.Reference); break; case ShopKind.Sharpening: condition = shop.Reference == "native_sharpening"; break; case ShopKind.Pocket: condition = shop.Reference == "native_pocket_slot"; break; case ShopKind.Motor: condition = shop.Reference == "native_big_motor" || shop.Reference == "native_dual_motors"; break; default: condition = false; break; } Require(condition, "Invalid shop content reference: " + shop.Key + " -> " + shop.Reference); } foreach (PlacementDefinition placement in Placements) { Require(ValidSceneName(placement.Marker) && Enum.IsDefined(typeof(PlacementKind), placement.Kind), "Invalid placement marker: " + placement.Marker); Require(Enum.IsDefined(typeof(SupportKind), placement.Support) && Finite(placement.FootprintWidth) && Finite(placement.FootprintDepth) && Finite(placement.ClearanceHeight) && placement.FootprintWidth >= 0f && placement.FootprintDepth >= 0f && placement.ClearanceHeight >= 0f && placement.FootprintWidth <= 50f && placement.FootprintDepth <= 50f && placement.ClearanceHeight <= 20f && placement.FootprintWidth == 0f == (placement.FootprintDepth == 0f) && (placement.ClearanceHeight == 0f || placement.FootprintWidth > 0f), "Invalid placement footprint or support rule: " + placement.Marker); Require(placement.Kind switch { PlacementKind.Npc => Npcs.Any((NpcDefinition npcDefinition) => npcDefinition.Key == placement.Reference), PlacementKind.Shop => Shops.Any((ShopDefinition shopDefinition) => shopDefinition.Key == placement.Reference), PlacementKind.Service => placement.Reference == "grill" || placement.Reference == "slot_machine", _ => ValidKey(placement.Reference), }, "Invalid marker reference: " + placement.Marker + " -> " + placement.Reference); } Require(Placements.Count((PlacementDefinition marker) => marker.Kind == PlacementKind.Player) == 1 && Placements.Count((PlacementDefinition marker) => marker.Kind == PlacementKind.Boat) == 1, "An island needs exactly one player spawn and one boat spawn."); foreach (NpcDefinition npc in Npcs) { Require(Placements.Count((PlacementDefinition marker) => marker.Kind == PlacementKind.Npc && marker.Reference == npc.Key) == 1, "An NPC needs exactly one placement: " + npc.Key); } foreach (ShopDefinition shop2 in Shops) { Require(Placements.Count((PlacementDefinition marker) => marker.Kind == PlacementKind.Shop && marker.Reference == shop2.Key) == 1, "A shop needs exactly one placement: " + shop2.Key); } foreach (EncounterDefinition encounter in Encounters) { Require(Items.Any((ItemDefinition item) => item.Key == encounter.Item && item.Kind == ItemKind.Boss) && Items.Any((ItemDefinition item) => item.Key == encounter.Trophy && item.Kind == ItemKind.Trophy) && encounter.NativeDonorId >= 0 && encounter.NativeDonorId <= 255 && encounter.Health > 0 && encounter.Damage > 0 && Finite(encounter.Force) && encounter.Force > 0f, "Invalid encounter: " + encounter.Key); } Require(Encounters.Select((EncounterDefinition encounterDefinition) => encounterDefinition.Item).Distinct(StringComparer.Ordinal).Count() == Encounters.Count, "A boss item cannot have multiple encounter configurations."); foreach (ItemDefinition boss in Items.Where((ItemDefinition item) => item.Kind == ItemKind.Boss)) { Require(Encounters.Count((EncounterDefinition encounterDefinition) => encounterDefinition.Item == boss.Key) == 1, "Every boss item needs one encounter configuration: " + boss.Key); } HashSet hashSet = new HashSet(StringComparer.Ordinal); foreach (QuestStageDefinition quest in Quests) { Require(Npcs.Any((NpcDefinition npcDefinition) => npcDefinition.Key == quest.Npc && npcDefinition.Role == NpcRole.Quest) && Items.Any((ItemDefinition item) => item.Key == quest.Trophy && item.Kind == ItemKind.Trophy) && quest.Requires != null && (quest.Requires.Length == 0 || hashSet.Contains(quest.Requires)) && Enum.IsDefined(typeof(RewardKind), quest.RewardKind), "Invalid or out-of-order quest stage: " + quest.Key); Require((quest.RewardKind == RewardKind.Lure) ? Lures.Any((LureDefinition lure) => lure.Key == quest.Reward && lure.Cost == 0) : (quest.Reward == "next_island"), "Invalid quest reward: " + quest.Key); hashSet.Add(quest.Key); } Require(Quests.Count == 0 || Quests.Count((QuestStageDefinition questStageDefinition) => questStageDefinition.RewardKind == RewardKind.Coordinates) == 1, "A progression chain must award next-island coordinates exactly once."); Require(Quests.Count == 0 || Quests[Quests.Count - 1].RewardKind == RewardKind.Coordinates, "Next-island coordinates must be the final progression stage."); return errors; void Require(bool flag, string message) { if (!flag) { errors.Add(message); } } } public void ValidateMarkers(IEnumerable names) { if (names == null) { throw new ArgumentNullException("names"); } Validate(); string[] array = names.ToArray(); if (array.Any((string name) => !ValidSceneName(name))) { throw new InvalidDataException("The scene contains an invalid placement marker name."); } if (array.Distinct(StringComparer.Ordinal).Count() != array.Length) { throw new InvalidDataException("The scene contains duplicate placement marker names."); } string[] array2 = Placements.Select((PlacementDefinition placement) => placement.Marker).Except(array, StringComparer.Ordinal).ToArray(); if (array2.Length != 0) { throw new InvalidDataException("Missing scene markers: " + string.Join(", ", array2)); } } public static void ValidatePackSet(IEnumerable definitions) { if (definitions == null) { throw new ArgumentNullException("definitions"); } ExpansionDefinition[] array = definitions.ToArray(); HashSet hashSet = new HashSet(StringComparer.Ordinal); HashSet hashSet2 = new HashSet(); HashSet hashSet3 = new HashSet(); HashSet items = new HashSet(); HashSet npcs = new HashSet(); HashSet hashSet4 = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet hashSet5 = new HashSet(StringComparer.OrdinalIgnoreCase); Dictionary<(ShopKind, int), string> dictionary = new Dictionary<(ShopKind, int), string>(); string text = null; ExpansionDefinition[] array2 = array; foreach (ExpansionDefinition expansionDefinition in array2) { if (expansionDefinition == null) { throw new InvalidDataException("An expansion pack entry is null."); } expansionDefinition.Validate(); if (text != null && text != expansionDefinition.GameBuild) { throw new InvalidDataException("Expansion packs target different native game builds."); } text = expansionDefinition.GameBuild; if (!hashSet.Add(expansionDefinition.Key) || !hashSet2.Add(expansionDefinition.IslandId) || !hashSet3.Add(expansionDefinition.ContentCollectionId) || !hashSet3.Add(expansionDefinition.EncounterCollectionId) || !hashSet4.Add(expansionDefinition.SceneBundle) || !hashSet5.Add(expansionDefinition.SceneName) || expansionDefinition.Items.Any((ItemDefinition item) => !items.Add(item.Id)) || expansionDefinition.Npcs.Any((NpcDefinition npc) => !npcs.Add(npc.Id))) { throw new InvalidDataException("Expansion pack identifiers collide: " + expansionDefinition.Key); } foreach (ShopDefinition item in expansionDefinition.Shops.Where((ShopDefinition shop) => shop.Kind == ShopKind.NativeItem || shop.Kind == ShopKind.NativeLure)) { (ShopKind, int) key = (item.Kind, item.NativeId); if (dictionary.TryGetValue(key, out var value) && value != item.NativeName) { throw new InvalidDataException("Expansion packs disagree about a native stock identity: " + item.Reference); } dictionary[key] = item.NativeName; } } array2 = array; foreach (ExpansionDefinition expansionDefinition2 in array2) { foreach (ShopDefinition item2 in expansionDefinition2.Shops.Where((ShopDefinition shop) => shop.Kind == ShopKind.NativeItem)) { if (items.Contains(item2.NativeId)) { throw new InvalidDataException("A native stock reference points at an expansion item: " + expansionDefinition2.Key + "/" + item2.Key); } } } } private static void CheckKeys(IEnumerable values, Func key, string kind, List errors) where T : class { HashSet hashSet = new HashSet(StringComparer.Ordinal); foreach (T value in values) { if (value == null || !ValidKey(key(value)) || !hashSet.Add(key(value))) { errors.Add("Missing, invalid or duplicate " + kind + " key."); } } } private static bool ValidKey(string value) { if (value != null) { return Regex.IsMatch(value, "^[a-z][a-z0-9_]{0,63}$"); } return false; } private static bool ValidSceneName(string value) { if (value != null) { return Regex.IsMatch(value, "^[A-Za-z][A-Za-z0-9_]{0,95}$"); } return false; } private static bool ValidFile(string value) { if (value != null && Regex.IsMatch(value, "^[a-z][a-z0-9_.-]{0,95}$")) { return !value.Contains(".."); } return false; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } public sealed class ItemDefinition { public string Key { get; set; } = ""; public string Title { get; set; } = ""; public int Id { get; set; } public int NativeDonorId { get; set; } = -1; public ItemKind Kind { get; set; } public string AssetKey { get; set; } = ""; public int Worth { get; set; } public int Cost { get; set; } public float CatchWeight { get; set; } public int Health { get; set; } } public sealed class LureDefinition { public string Key { get; set; } = ""; public string Title { get; set; } = ""; public string Description { get; set; } = ""; public string AssetKey { get; set; } = ""; public int Cost { get; set; } public float LossPercent { get; set; } public float CatchTimeMin { get; set; } public float CatchTimeMax { get; set; } public List Catches { get; set; } = new List(); } public sealed class CatchDefinition { public string Item { get; set; } = ""; public float Weight { get; set; } } public sealed class NpcDefinition { public string Key { get; set; } = ""; public string Title { get; set; } = ""; public int Id { get; set; } public NpcRole Role { get; set; } } public sealed class ShopDefinition { public string Key { get; set; } = ""; public ShopKind Kind { get; set; } public string Reference { get; set; } = ""; public int NativeId { get; set; } = -1; public string NativeName { get; set; } = ""; } public sealed class PlacementDefinition { public string Marker { get; set; } = ""; public PlacementKind Kind { get; set; } public string Reference { get; set; } = ""; public SupportKind Support { get; set; } public float FootprintWidth { get; set; } public float FootprintDepth { get; set; } public float ClearanceHeight { get; set; } public SupportKind GetSupportKind() { if (Support != SupportKind.Inferred) { return Support; } SupportKind result; switch (Kind) { case PlacementKind.Boat: result = SupportKind.Water; break; case PlacementKind.Shop: case PlacementKind.Table: result = SupportKind.Surface; break; case PlacementKind.Player: case PlacementKind.Npc: case PlacementKind.Service: case PlacementKind.Fishing: case PlacementKind.TablePlayer: result = SupportKind.Ground; break; default: result = SupportKind.None; break; } return result; } } public sealed class EncounterDefinition { public string Key { get; set; } = ""; public string Item { get; set; } = ""; public string Trophy { get; set; } = ""; public int NativeDonorId { get; set; } public int Health { get; set; } public int Damage { get; set; } public float Force { get; set; } } public sealed class QuestStageDefinition { public string Key { get; set; } = ""; public string Npc { get; set; } = ""; public string Trophy { get; set; } = ""; public string Requires { get; set; } = ""; public RewardKind RewardKind { get; set; } public string Reward { get; set; } = ""; } } namespace HowToFish.ExpansionKit.World { public readonly struct SurfaceSample { public Vector3 Point { get; } public Vector3 Normal { get; } public SurfaceSample(Vector3 point, Vector3 normal) { Point = point; Normal = normal; } } public sealed class TriangleSurface { public const int MaximumIndexedCellVisits = 1000000; private readonly Vector3[] vertices; private readonly int[] triangles; private readonly Dictionary> cells = new Dictionary>(); private readonly float cellSize; public Vector3 Minimum { get; } public Vector3 Maximum { get; } public float Area { get; } public TriangleSurface(IReadOnlyList vertices, IReadOnlyList triangles, float cellSize = 1f) { if (vertices == null) { throw new ArgumentNullException("vertices"); } if (triangles == null) { throw new ArgumentNullException("triangles"); } if (!IsFinite(cellSize) || cellSize <= 0f) { throw new ArgumentOutOfRangeException("cellSize"); } if (triangles.Count == 0 || triangles.Count % 3 != 0) { throw new ArgumentException("Triangle indices must contain one or more complete triangles.", "triangles"); } this.vertices = new Vector3[vertices.Count]; for (int i = 0; i < vertices.Count; i++) { Vector3 vector = vertices[i]; if (!IsFinite(vector.X) || !IsFinite(vector.Y) || !IsFinite(vector.Z)) { throw new ArgumentException("Surface vertices must be finite.", "vertices"); } this.vertices[i] = vector; } this.triangles = new int[triangles.Count]; this.cellSize = cellSize; bool flag = false; float minX = 0f; float minY = 0f; float minZ = 0f; float maxX = 0f; float maxY = 0f; float maxZ = 0f; float num = 0f; long num2 = 0L; for (int j = 0; j < triangles.Count; j += 3) { int num3 = triangles[j]; int num4 = triangles[j + 1]; int num5 = triangles[j + 2]; ValidateVertexIndex(num3, vertices.Count, triangles); ValidateVertexIndex(num4, vertices.Count, triangles); ValidateVertexIndex(num5, vertices.Count, triangles); this.triangles[j] = num3; this.triangles[j + 1] = num4; this.triangles[j + 2] = num5; Vector3 vector2 = this.vertices[num3]; Vector3 vector3 = this.vertices[num4]; Vector3 vector4 = this.vertices[num5]; Vector3 vector5 = Cross(Subtract(vector3, vector2), Subtract(vector4, vector2)); float num6 = MathF.Sqrt(vector5.X * vector5.X + vector5.Y * vector5.Y + vector5.Z * vector5.Z); if (!IsFinite(num6)) { throw new ArgumentException("Surface triangle area must be finite.", "triangles"); } num += num6 * 0.5f; if (!flag) { minX = (maxX = vector2.X); minY = (maxY = vector2.Y); minZ = (maxZ = vector2.Z); flag = true; } Include(vector2, ref minX, ref minY, ref minZ, ref maxX, ref maxY, ref maxZ); Include(vector3, ref minX, ref minY, ref minZ, ref maxX, ref maxY, ref maxZ); Include(vector4, ref minX, ref minY, ref minZ, ref maxX, ref maxY, ref maxZ); float num7 = MathF.Min(vector2.X, MathF.Min(vector3.X, vector4.X)); float num8 = MathF.Min(vector2.Z, MathF.Min(vector3.Z, vector4.Z)); float num9 = MathF.Max(vector2.X, MathF.Max(vector3.X, vector4.X)); float num10 = MathF.Max(vector2.Z, MathF.Max(vector3.Z, vector4.Z)); int num11 = FloorToInt(num7 / cellSize); int num12 = FloorToInt(num9 / cellSize); int num13 = FloorToInt(num8 / cellSize); int num14 = FloorToInt(num10 / cellSize); long num15 = (long)num12 - (long)num11 + 1; long num16 = (long)num14 - (long)num13 + 1; if (num15 > 1000000 || num16 > 1000000 || num15 * num16 > 1000000 - num2) { throw new ArgumentOutOfRangeException("cellSize", "The surface exceeds its spatial-index budget."); } num2 += num15 * num16; for (int k = num11; k <= num12; k++) { for (int l = num13; l <= num14; l++) { long key = Key(k, l); if (!cells.TryGetValue(key, out List value)) { value = new List(); cells.Add(key, value); } value.Add(j); } } } if (!IsFinite(num) || num <= 0f) { throw new ArgumentException("Surface must have positive finite area.", "triangles"); } Minimum = new Vector3(minX, minY, minZ); Maximum = new Vector3(maxX, maxY, maxZ); Area = num; } public bool TrySample(Vector3 world, out SurfaceSample sample) { if (!IsFinite(world.X) || !IsFinite(world.Z)) { throw new ArgumentException("Sample coordinates must be finite.", "world"); } sample = default(SurfaceSample); if (!cells.TryGetValue(Key(FloorToInt(world.X / cellSize), FloorToInt(world.Z / cellSize)), out List value)) { return false; } bool flag = false; Vector3 point = default(Vector3); Vector3 normal = default(Vector3); foreach (int item in value) { Vector3 vector = vertices[triangles[item]]; Vector3 vector2 = vertices[triangles[item + 1]]; Vector3 vector3 = vertices[triangles[item + 2]]; if (Barycentric(world, vector, vector2, vector3, out var u, out var v, out var w)) { Vector3 vector4 = new Vector3(vector.X * u + vector2.X * v + vector3.X * w, vector.Y * u + vector2.Y * v + vector3.Y * w, vector.Z * u + vector2.Z * v + vector3.Z * w); if (!flag || !(vector4.Y <= point.Y)) { Vector3 vector5 = Normalize(Cross(Subtract(vector2, vector), Subtract(vector3, vector))); point = vector4; normal = ((vector5.Y < 0f) ? new Vector3(0f - vector5.X, 0f - vector5.Y, 0f - vector5.Z) : vector5); flag = true; } } } if (flag) { sample = new SurfaceSample(point, normal); } return flag; } private static void ValidateVertexIndex(int index, int count, IReadOnlyList argument) { if (index < 0 || index >= count) { throw new ArgumentException("A triangle index is outside the vertex array.", "argument"); } } private static void Include(Vector3 value, ref float minX, ref float minY, ref float minZ, ref float maxX, ref float maxY, ref float maxZ) { minX = MathF.Min(minX, value.X); minY = MathF.Min(minY, value.Y); minZ = MathF.Min(minZ, value.Z); maxX = MathF.Max(maxX, value.X); maxY = MathF.Max(maxY, value.Y); maxZ = MathF.Max(maxZ, value.Z); } private static bool Barycentric(Vector3 p, Vector3 a, Vector3 b, Vector3 c, out float u, out float v, out float w) { float num = b.X - a.X; float num2 = b.Z - a.Z; float num3 = c.X - a.X; float num4 = c.Z - a.Z; float num5 = p.X - a.X; float num6 = p.Z - a.Z; float num7 = num * num4 - num3 * num2; u = (v = (w = 0f)); if (MathF.Abs(num7) < 1E-09f) { return false; } v = (num5 * num4 - num3 * num6) / num7; w = (num * num6 - num5 * num2) / num7; u = 1f - v - w; if (u >= -0.0001f && v >= -0.0001f) { return w >= -0.0001f; } return false; } private static Vector3 Subtract(Vector3 left, Vector3 right) { return new Vector3(left.X - right.X, left.Y - right.Y, left.Z - right.Z); } private static Vector3 Cross(Vector3 left, Vector3 right) { return new Vector3(left.Y * right.Z - left.Z * right.Y, left.Z * right.X - left.X * right.Z, left.X * right.Y - left.Y * right.X); } private static Vector3 Normalize(Vector3 value) { float num = MathF.Sqrt(value.X * value.X + value.Y * value.Y + value.Z * value.Z); return new Vector3(value.X / num, value.Y / num, value.Z / num); } private static int FloorToInt(float value) { if (!IsFinite(value) || (double)value < -2147483648.0 || (double)value > 2147483647.0) { throw new ArgumentOutOfRangeException("value"); } return (int)MathF.Floor(value); } private static bool IsFinite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } private static long Key(int x, int z) { return ((long)x << 32) ^ (uint)z; } } public sealed class GroundCoverSettings { public float MeshSize { get; } public float Density { get; } public float PositionRandomness { get; } public float RotationRandomness { get; } public float MinimumScale { get; } public float MaximumScale { get; } public float MinimumNormalY { get; } public float SeaLevel { get; } public float MinimumHeightAboveSea { get; } public float EdgeMargin { get; } public int Seed { get; } public float Step => MeshSize / Density; public GroundCoverSettings(float meshSize, float density, float positionRandomness, float rotationRandomness, float minimumScale, float maximumScale, float minimumNormalY, float seaLevel, float minimumHeightAboveSea, float edgeMargin, int seed) { if (!FinitePositive(meshSize)) { throw new ArgumentOutOfRangeException("meshSize"); } if (!FinitePositive(density)) { throw new ArgumentOutOfRangeException("density"); } if (!Finite(positionRandomness) || positionRandomness < 0f || positionRandomness > 1f) { throw new ArgumentOutOfRangeException("positionRandomness"); } if (!Finite(rotationRandomness) || rotationRandomness < 0f || rotationRandomness > 360f) { throw new ArgumentOutOfRangeException("rotationRandomness"); } if (!FinitePositive(minimumScale)) { throw new ArgumentOutOfRangeException("minimumScale"); } if (!Finite(maximumScale) || maximumScale < minimumScale) { throw new ArgumentOutOfRangeException("maximumScale"); } if (!Finite(minimumNormalY) || minimumNormalY < -1f || minimumNormalY > 1f) { throw new ArgumentOutOfRangeException("minimumNormalY"); } if (!Finite(seaLevel)) { throw new ArgumentOutOfRangeException("seaLevel"); } if (!Finite(minimumHeightAboveSea) || minimumHeightAboveSea < 0f) { throw new ArgumentOutOfRangeException("minimumHeightAboveSea"); } if (!Finite(edgeMargin) || edgeMargin < 0f) { throw new ArgumentOutOfRangeException("edgeMargin"); } MeshSize = meshSize; Density = density; PositionRandomness = positionRandomness; RotationRandomness = rotationRandomness; MinimumScale = minimumScale; MaximumScale = maximumScale; MinimumNormalY = minimumNormalY; SeaLevel = seaLevel; MinimumHeightAboveSea = minimumHeightAboveSea; EdgeMargin = edgeMargin; Seed = seed; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } private static bool FinitePositive(float value) { if (Finite(value)) { return value > 0f; } return false; } } public readonly struct GroundCoverPlacement { public Vector3 Point { get; } public Vector3 Normal { get; } public float RollDegrees { get; } public float Scale { get; } public GroundCoverPlacement(Vector3 point, Vector3 normal, float rollDegrees, float scale) { Point = point; Normal = normal; RollDegrees = rollDegrees; Scale = scale; } } public enum GroundCoverExclusion { None, Covered, Blocked } public sealed class GroundCoverPlanStatistics { public int Candidates { get; internal set; } public int OffSurface { get; internal set; } public int Steep { get; internal set; } public int Low { get; internal set; } public int Edge { get; internal set; } public int Covered { get; internal set; } public int Blocked { get; internal set; } public int Thinned { get; internal set; } public int Accepted { get; internal set; } } public sealed class GroundCoverPlan { public IReadOnlyList Placements { get; } public GroundCoverPlanStatistics Statistics { get; } internal GroundCoverPlan(IReadOnlyList placements, GroundCoverPlanStatistics statistics) { Placements = placements; Statistics = statistics; } } public sealed class GroundCoverPlanner { public const int MaximumCandidates = 1000000; private readonly TriangleSurface surface; private readonly GroundCoverSettings settings; public GroundCoverPlanner(TriangleSurface surface, GroundCoverSettings settings) { this.surface = surface ?? throw new ArgumentNullException("surface"); this.settings = settings ?? throw new ArgumentNullException("settings"); } public GroundCoverPlan Plan(int maximumPlacements, Func exclude) { if (maximumPlacements <= 0) { throw new ArgumentOutOfRangeException("maximumPlacements"); } if (exclude == null) { throw new ArgumentNullException("exclude"); } Random random = new Random(settings.Seed); List list = new List(); GroundCoverPlanStatistics groundCoverPlanStatistics = new GroundCoverPlanStatistics(); float step = settings.Step; int num = CeilingToInt((surface.Maximum.X - surface.Minimum.X) / step); int num2 = CeilingToInt((surface.Maximum.Z - surface.Minimum.Z) / step); if ((long)num * (long)num2 > 1000000) { throw new ArgumentOutOfRangeException("settings", "The ground-cover grid exceeds its candidate budget."); } for (int i = 0; i < num; i++) { for (int j = 0; j < num2; j++) { float num3 = Range(random, 0f - settings.PositionRandomness, settings.PositionRandomness) * settings.MeshSize * 0.5f; float num4 = Range(random, 0f - settings.PositionRandomness, settings.PositionRandomness) * settings.MeshSize * 0.5f; float rollDegrees = Range(random, 0f - settings.RotationRandomness, settings.RotationRandomness); float scale = Range(random, settings.MinimumScale, settings.MaximumScale); groundCoverPlanStatistics.Candidates++; Vector3 world = new Vector3(surface.Minimum.X + ((float)i + 0.5f) * step + num3, 0f, surface.Minimum.Z + ((float)j + 0.5f) * step + num4); if (!surface.TrySample(world, out var sample)) { groundCoverPlanStatistics.OffSurface++; continue; } if (sample.Normal.Y < settings.MinimumNormalY) { groundCoverPlanStatistics.Steep++; continue; } if (sample.Point.Y < settings.SeaLevel + settings.MinimumHeightAboveSea) { groundCoverPlanStatistics.Low++; continue; } if (!HasEdgeClearance(sample.Point)) { groundCoverPlanStatistics.Edge++; continue; } GroundCoverPlacement groundCoverPlacement = new GroundCoverPlacement(sample.Point, sample.Normal, rollDegrees, scale); switch (exclude(groundCoverPlacement)) { case GroundCoverExclusion.Covered: groundCoverPlanStatistics.Covered++; break; case GroundCoverExclusion.Blocked: groundCoverPlanStatistics.Blocked++; break; default: throw new ArgumentOutOfRangeException("exclude", "The exclusion callback returned an undefined reason."); case GroundCoverExclusion.None: list.Add(groundCoverPlacement); break; } } } if (list.Count > maximumPlacements) { float num5 = (float)maximumPlacements / (float)list.Count; List list2 = new List(maximumPlacements); for (int k = 0; k < list.Count; k++) { if (list2.Count >= maximumPlacements) { break; } if ((float)k * 0.618034f % 1f < num5) { list2.Add(list[k]); } } groundCoverPlanStatistics.Thinned = list.Count - list2.Count; list = list2; } groundCoverPlanStatistics.Accepted = list.Count; return new GroundCoverPlan(list.AsReadOnly(), groundCoverPlanStatistics); } private bool HasEdgeClearance(Vector3 point) { float edgeMargin = settings.EdgeMargin; if (SampleOffset(point, edgeMargin, edgeMargin) && SampleOffset(point, 0f - edgeMargin, edgeMargin) && SampleOffset(point, edgeMargin, 0f - edgeMargin)) { return SampleOffset(point, 0f - edgeMargin, 0f - edgeMargin); } return false; } private bool SampleOffset(Vector3 point, float x, float z) { SurfaceSample sample; return surface.TrySample(new Vector3(point.X + x, point.Y, point.Z + z), out sample); } private static float Range(Random random, float minimum, float maximum) { return minimum + (float)random.NextDouble() * (maximum - minimum); } private static int CeilingToInt(float value) { if (float.IsNaN(value) || float.IsInfinity(value) || value < 0f || (double)value > 2147483647.0) { throw new ArgumentOutOfRangeException("value"); } return (int)MathF.Ceiling(value); } } } namespace HowToFish.ExpansionKit.Progression { public sealed class InterludeRouteSelection { internal int RequiredNativeUnlockCount { get; } internal int ContinuationIslandId { get; } public int NativeUnlockCount { get; } public bool IsEligible { get; } public bool HadContinuationAccess { get; } internal InterludeRouteSelection(int requiredNativeUnlockCount, int continuationIslandId, int nativeUnlockCount, bool isEligible, bool hadContinuationAccess) { RequiredNativeUnlockCount = requiredNativeUnlockCount; ContinuationIslandId = continuationIslandId; NativeUnlockCount = nativeUnlockCount; IsEligible = isEligible; HadContinuationAccess = hadContinuationAccess; } } public sealed class InterludeRoutePolicy { public int RequiredNativeUnlockCount { get; } public int ContinuationIslandId { get; } public InterludeRoutePolicy(int requiredNativeUnlockCount, int continuationIslandId) { if (requiredNativeUnlockCount < 1 || requiredNativeUnlockCount > 255) { throw new ArgumentOutOfRangeException("requiredNativeUnlockCount"); } if (continuationIslandId < 0 || continuationIslandId > 255) { throw new ArgumentOutOfRangeException("continuationIslandId"); } RequiredNativeUnlockCount = requiredNativeUnlockCount; ContinuationIslandId = continuationIslandId; } public static bool IsUnlocked(int zeroBasedIslandId, int exclusiveNativeUnlockCount) { if (zeroBasedIslandId < 0 || zeroBasedIslandId > 255) { throw new ArgumentOutOfRangeException("zeroBasedIslandId"); } ValidateUnlockCount(exclusiveNativeUnlockCount); return zeroBasedIslandId < exclusiveNativeUnlockCount; } public bool IsEligible(int exclusiveNativeUnlockCount) { ValidateUnlockCount(exclusiveNativeUnlockCount); return exclusiveNativeUnlockCount >= RequiredNativeUnlockCount; } public InterludeRouteSelection CaptureAtSaveSelection(int exclusiveNativeUnlockCount) { ValidateUnlockCount(exclusiveNativeUnlockCount); return new InterludeRouteSelection(RequiredNativeUnlockCount, ContinuationIslandId, exclusiveNativeUnlockCount, IsEligible(exclusiveNativeUnlockCount), IsUnlocked(ContinuationIslandId, exclusiveNativeUnlockCount)); } public bool CanAccessContinuation(InterludeRouteSelection selection, bool interludeCompleted, int currentExclusiveNativeUnlockCount) { if (selection == null) { throw new ArgumentNullException("selection"); } if (selection.RequiredNativeUnlockCount != RequiredNativeUnlockCount || selection.ContinuationIslandId != ContinuationIslandId) { throw new ArgumentException("The selection snapshot belongs to a different route policy.", "selection"); } ValidateUnlockCount(currentExclusiveNativeUnlockCount); return selection.HadContinuationAccess || interludeCompleted; } private static void ValidateUnlockCount(int count) { if (count < 0 || count > 255) { throw new ArgumentOutOfRangeException("count"); } } } } namespace HowToFish.ExpansionKit.Packs { public static class NativePackCatalog { private static readonly HashSet Weapons = new HashSet { 54, 57, 58, 60, 63, 64, 66, 68, 69, 70 }; private static readonly HashSet RangedWeapons = new HashSet { 54, 66, 68, 69, 70 }; private static readonly int[][] Npcs = new int[6][] { new int[0], new int[1] { 1 }, new int[2] { 2, 3 }, new int[3] { 4, 5, 6 }, new int[6] { 4, 7, 8, 9, 10, 11 }, new int[3] { 12, 13, 14 } }; public static bool Contains(ContentDomain domain, int id) { switch (domain) { case ContentDomain.Item: if (id >= 0 && id <= 85) { return id != 30; } return false; case ContentDomain.Lure: if (id >= 0) { return id <= 16; } return false; case ContentDomain.Npc: if (id >= 1) { return id <= 14; } return false; case ContentDomain.Island: if (id >= 1) { return id <= 5; } return false; case ContentDomain.Attachment: if (id >= 0) { return id <= 6; } return false; case ContentDomain.Ammunition: if (id >= 1) { return id <= 12; } return false; case ContentDomain.BoatRadar: return id == 0; case ContentDomain.Sharpening: if (id >= 1) { return id <= 15; } return false; case ContentDomain.Pocket: if (id >= 1) { return id <= 5; } return false; case ContentDomain.Motor: if (id != 1) { return id == 2; } return true; default: return false; } } public static bool IsCreature(int id) { if ((id < 0 || id > 52 || id == 30) && id != 56) { if (id >= 79) { return id <= 85; } return false; } return true; } public static bool IsRangedWeapon(int id) { return RangedWeapons.Contains(id); } public static bool SupportsItemKind(int id, PackItemKind kind) { if (!Contains(ContentDomain.Item, id)) { return false; } switch (kind) { case PackItemKind.Fish: if (id >= 0 && id <= 52) { return id != 30; } return false; case PackItemKind.Creature: return IsCreature(id); case PackItemKind.Food: if (id >= 81) { return id <= 85; } return false; case PackItemKind.Weapon: return Weapons.Contains(id); case PackItemKind.Rod: if (id != 59) { return id == 61; } return true; case PackItemKind.Item: return true; default: return false; } } public static bool HasNpcDonor(int island, int npc) { if (island >= 1 && island <= 5) { return Array.IndexOf(Npcs[island], npc) >= 0; } return false; } } internal static class PackCanonical { internal const int FingerprintVersion = 2; internal static T Copy(T value) where T : class { return (T)CopyValue(value); } private static object? CopyValue(object? value) { if (value == null || value is string || value.GetType().IsValueType) { return value; } if (value is IList list) { IList list2 = (IList)Activator.CreateInstance(value.GetType()); { foreach (object item in list) { list2.Add(CopyValue(item)); } return list2; } } object obj = Activator.CreateInstance(value.GetType()); PropertyInfo[] array = Properties(value.GetType()); foreach (PropertyInfo propertyInfo in array) { propertyInfo.SetValue(obj, CopyValue(propertyInfo.GetValue(value))); } return obj; } internal static string Hash(object value) { return Hash(value, LogicalType, versioned: true); } internal static IEnumerable LegacyHashes(object value) { string[] array = new string[2] { "0.4.0.0", "0.5.0.0" }; foreach (string version in array) { string[] array2 = new string[2] { "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" }; foreach (string core in array2) { yield return Hash(value, (Type type) => LegacyType(type, version, core), versioned: false); } } } private static string Hash(object value, Func typeName, bool versioned) { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { if (versioned) { binaryWriter.Write("ExpansionKit canonical"); binaryWriter.Write(2); } Write(binaryWriter, value, typeName); } using SHA256 sHA = SHA256.Create(); return string.Concat(from x in sHA.ComputeHash(memoryStream.ToArray()) select x.ToString("x2")); } private static string LogicalType(Type type) { if (type.IsArray) { return LogicalType(type.GetElementType()) + "[]"; } if (!type.IsGenericType) { return type.FullName; } return type.GetGenericTypeDefinition().FullName + "<" + string.Join(",", type.GetGenericArguments().Select(LogicalType)) + ">"; } private static string LegacyType(Type type, string sdkVersion, string core) { if (type.IsArray) { return LegacyType(type.GetElementType(), sdkVersion, core) + "[]"; } if (!type.IsGenericType) { return type.FullName; } return type.GetGenericTypeDefinition().FullName + "[" + string.Join(",", type.GetGenericArguments().Select(delegate(Type argument) { string text = ((argument.Assembly == typeof(PackDefinition).Assembly) ? ("HowToFish.ExpansionKit, Version=" + sdkVersion + ", Culture=neutral, PublicKeyToken=null") : ((argument.Assembly == typeof(string).Assembly) ? core : argument.Assembly.FullName)); return "[" + LegacyType(argument, sdkVersion, core) + ", " + text + "]"; })) + "]"; } private static PropertyInfo[] Properties(Type type) { return (from x in type.GetProperties(BindingFlags.Instance | BindingFlags.Public) where x.CanRead && x.CanWrite select x).OrderBy((PropertyInfo x) => x.Name, StringComparer.Ordinal).ToArray(); } private static void Write(BinaryWriter writer, object? value, Func typeName) { if (value == null) { writer.Write((byte)0); return; } writer.Write((byte)1); writer.Write(typeName(value.GetType())); if (value is string value2) { writer.Write(value2); } else if (value is float value3) { writer.Write(value3); } else if (value is bool value4) { writer.Write(value4); } else if (value.GetType().IsValueType) { writer.Write(Convert.ToInt64(value)); } else if (value is IList source) { object[] array = source.Cast().ToArray(); if (array.Length != 0 && array[0].GetType().GetProperty("Key") != null) { array = array.OrderBy((object x) => (string)x.GetType().GetProperty("Key").GetValue(x), StringComparer.Ordinal).ToArray(); } writer.Write(array.Length); object[] array2 = array; foreach (object value5 in array2) { Write(writer, value5, typeName); } } else { PropertyInfo[] array3 = Properties(value.GetType()); writer.Write(array3.Length); PropertyInfo[] array4 = array3; foreach (PropertyInfo propertyInfo in array4) { writer.Write(propertyInfo.Name); Write(writer, propertyInfo.GetValue(value), typeName); } } } } public enum ContentDomain { Item, Lure, Npc, Island, Quest, Shop, Loot, Attachment, Ammunition, BoatRadar, Sharpening, Pocket, Motor } public enum RecipeImplementation { NativeClone, External } public enum PackItemKind { Item, Food, Fish, Creature, Weapon, Rod } public enum PackNpcRole { Merchant, DeliveryQuest, External } public enum PackShopKind { Item, Lure, Attachment, Ammunition, BoatRadar, Sharpening, Pocket, Motor } public enum ShopCapPolicy { Unlimited, PerPlayer, Shared } public enum QuestObjectiveKind { DeliverItem, CatchItem } public enum QuestRewardKind { Item, Lure, Coordinates, Money } public enum CatchPatchMode { Additive, ExclusiveReplace } public enum ReloadPolicy { Donor, Magazine, SingleRound } public enum MagazinePolicy { Donor, Fixed, PerUpgrade } public sealed class PackDefinition { public const int CurrentSchema = 1; public const string SupportedGameBuild = "25127368"; public int SchemaVersion { get; set; } = 1; public string Key { get; set; } = ""; public string Title { get; set; } = ""; public string Version { get; set; } = "1.0.0"; public string GameBuild { get; set; } = "25127368"; public List Dependencies { get; set; } = new List(); public List ExtensionHooks { get; set; } = new List(); public List Collections { get; set; } = new List(); public List Items { get; set; } = new List(); public List Lures { get; set; } = new List(); public List CatchPatches { get; set; } = new List(); public List Npcs { get; set; } = new List(); public List Shops { get; set; } = new List(); public List Islands { get; set; } = new List(); public List Quests { get; set; } = new List(); public List Loot { get; set; } = new List(); public IReadOnlyList Inspect() { return PackValidation.Inspect(this); } public void Validate() { PackValidation.ThrowIfInvalid(Inspect()); } } public sealed class PackDependency { public string Key { get; set; } = ""; public string? MinimumVersion { get; set; } public string? ExactVersion { get; set; } } public sealed class NetworkCollectionRecipe { public string Key { get; set; } = ""; public ushort Id { get; set; } public int Capacity { get; set; } = 256; } public sealed class NetworkBinding { public ushort CollectionId { get; set; } public ushort Slot { get; set; } } public abstract class PackRecipe { public string Key { get; set; } = ""; public RecipeImplementation Implementation { get; set; } public string? ExtensionHook { get; set; } } public sealed class VectorRecipe { public float X { get; set; } public float Y { get; set; } public float Z { get; set; } } public sealed class PoseRecipe { public VectorRecipe Position { get; set; } = new VectorRecipe(); public float Yaw { get; set; } } public sealed class PlacementRecipe { public string? Marker { get; set; } public PoseRecipe? Pose { get; set; } } public sealed class ArtRecipe { public string Bundle { get; set; } = ""; public string Sha256 { get; set; } = ""; public string Prefab { get; set; } = ""; public VectorRecipe Position { get; set; } = new VectorRecipe(); public VectorRecipe Rotation { get; set; } = new VectorRecipe(); public VectorRecipe Scale { get; set; } = new VectorRecipe { X = 1f, Y = 1f, Z = 1f }; public PoseRecipe? Grip { get; set; } } public sealed class ItemRecipe : PackRecipe { public byte Id { get; set; } public string Title { get; set; } = ""; public PackItemKind Kind { get; set; } public string? NativeDonor { get; set; } public NetworkBinding Network { get; set; } = new NetworkBinding(); public ArtRecipe? Art { get; set; } public int? Worth { get; set; } public int? Cost { get; set; } public float? Health { get; set; } public float? HealthRestored { get; set; } public float? FoodValue { get; set; } public bool? Edible { get; set; } public bool? Cookable { get; set; } public bool? Cooked { get; set; } public string? CookedItem { get; set; } public float? BodyDamageFactor { get; set; } public float? CrewHealthFactor { get; set; } public float? CrewDamageFactor { get; set; } public WeaponRecipe? Weapon { get; set; } public RodRecipe? Rod { get; set; } } public sealed class WeaponRecipe { public float? DamageScale { get; set; } public List UpgradeDamageScales { get; set; } = new List(); public MagazinePolicy MagazinePolicy { get; set; } public int? MagazineSize { get; set; } public List UpgradeMagazineSizes { get; set; } = new List(); public ReloadPolicy ReloadPolicy { get; set; } public float? ReloadSeconds { get; set; } public float? ShotDelaySeconds { get; set; } public List? AllowedAttachmentIds { get; set; } } public sealed class RodRecipe { public float? MaximumLineLength { get; set; } public float? LineStrengthScale { get; set; } public float? ReelingSpeedScale { get; set; } public List UpgradeLineStrengthScales { get; set; } = new List(); public List UpgradeReelingSpeedScales { get; set; } = new List(); } public sealed class CatchRecipe { public string Item { get; set; } = ""; public float Weight { get; set; } } public sealed class LureRecipe : PackRecipe { public byte Id { get; set; } public string Title { get; set; } = ""; public string Description { get; set; } = ""; public string? NativeVisualDonor { get; set; } public ArtRecipe? Art { get; set; } public int? Cost { get; set; } public float? LossPercent { get; set; } public float? CatchTimeMin { get; set; } public float? CatchTimeMax { get; set; } public List Catches { get; set; } = new List(); } public sealed class CatchTablePatch : PackRecipe { public string TargetLure { get; set; } = ""; public CatchPatchMode Mode { get; set; } public List Catches { get; set; } = new List(); } public sealed class NpcRecipe : PackRecipe { public byte Id { get; set; } public string Title { get; set; } = ""; public string? NativeDonorIsland { get; set; } public string? NativeDonor { get; set; } public NetworkBinding? Network { get; set; } public string Island { get; set; } = ""; public PlacementRecipe Placement { get; set; } = new PlacementRecipe(); public PackNpcRole Role { get; set; } public List Dialogue { get; set; } = new List(); public string? Quest { get; set; } public ArtRecipe? Art { get; set; } } public sealed class ShopRecipe : PackRecipe { public string Island { get; set; } = ""; public PlacementRecipe Placement { get; set; } = new PlacementRecipe(); public PackShopKind Kind { get; set; } public string Content { get; set; } = ""; public int? Cost { get; set; } public ShopCapPolicy CapPolicy { get; set; } public int? Cap { get; set; } } public sealed class RouteRecipe { public string AfterIsland { get; set; } = ""; public string BeforeIsland { get; set; } = ""; public int Priority { get; set; } } public sealed class IslandRecipe : PackRecipe { public byte Id { get; set; } public string Title { get; set; } = ""; public string SceneBundle { get; set; } = ""; public string SceneSha256 { get; set; } = ""; public string SceneName { get; set; } = ""; public string SceneRoot { get; set; } = ""; public int UnlockThreshold { get; set; } public RouteRecipe? Route { get; set; } public PlacementRecipe PlayerSpawn { get; set; } = new PlacementRecipe(); public PlacementRecipe BoatSpawn { get; set; } = new PlacementRecipe(); public VectorRecipe WorldMapPosition { get; set; } = new VectorRecipe(); } public sealed class QuestObjectiveRecipe { public QuestObjectiveKind Kind { get; set; } public string Item { get; set; } = ""; public int Count { get; set; } } public sealed class QuestRewardRecipe { public QuestRewardKind Kind { get; set; } public string? Content { get; set; } public int Amount { get; set; } = 1; } public sealed class QuestRecipe : PackRecipe { public string Title { get; set; } = ""; public List Requires { get; set; } = new List(); public List Objectives { get; set; } = new List(); public List Rewards { get; set; } = new List(); } public sealed class LootDropRecipe { public string Item { get; set; } = ""; public int FixedQuantity { get; set; } public int PerPlayerQuantity { get; set; } } public sealed class LootRecipe : PackRecipe { public string Creature { get; set; } = ""; public List Drops { get; set; } = new List(); } public sealed class PackQuestProgress { public string Quest { get; set; } = ""; public List Counts { get; set; } = new List(); public bool Claimed { get; set; } } public sealed class PackQuestSnapshot { public int SchemaVersion { get; set; } = 1; public List Quests { get; set; } = new List(); } public sealed class PackQuestJournal { private readonly Dictionary recipes; private readonly Dictionary progress; public PackQuestJournal(PackRegistry registry, PackQuestSnapshot? saved = null) { if (registry == null) { throw new ArgumentNullException("registry"); } recipes = registry.Packs.SelectMany((PackDefinition p) => p.Quests.Select((QuestRecipe q) => (Key: p.Key + ":" + q.Key, Recipe: q))).ToDictionary<(string, QuestRecipe), string, QuestRecipe>(((string Key, QuestRecipe Recipe) x) => x.Key, ((string Key, QuestRecipe Recipe) x) => x.Recipe, StringComparer.Ordinal); progress = recipes.ToDictionary, string, PackQuestProgress>((KeyValuePair x) => x.Key, (KeyValuePair x) => new PackQuestProgress { Quest = x.Key, Counts = Enumerable.Repeat(0, x.Value.Objectives.Count).ToList() }, StringComparer.Ordinal); if (saved == null) { return; } if (saved.SchemaVersion != 1 || saved.Quests == null || saved.Quests.Count > 8192 || saved.Quests.Any((PackQuestProgress x) => x == null)) { throw new InvalidDataException("Invalid bounded quest snapshot."); } HashSet hashSet = new HashSet(StringComparer.Ordinal); foreach (PackQuestProgress quest in saved.Quests) { if (quest.Quest == null || !hashSet.Add(quest.Quest) || !recipes.TryGetValue(quest.Quest, out QuestRecipe recipe) || quest.Counts == null || quest.Counts.Count != recipe.Objectives.Count || quest.Counts.Where((int x, int i) => x < 0 || x > recipe.Objectives[i].Count).Any()) { throw new InvalidDataException("Invalid, duplicate or missing quest in snapshot: " + quest.Quest); } progress[quest.Quest] = PackCanonical.Copy(quest); } foreach (PackQuestProgress value in progress.Values) { if (value.Claimed && (!Complete(value.Quest) || !Unlocked(value.Quest))) { throw new InvalidDataException("Claimed quest lacks objectives or claimed prerequisites: " + value.Quest); } } } public PackQuestSnapshot Snapshot() { return new PackQuestSnapshot { Quests = progress.Values.OrderBy((PackQuestProgress x) => x.Quest, StringComparer.Ordinal).Select(PackCanonical.Copy).ToList() }; } public bool IsClaimed(string quest) { return Get(quest).Claimed; } public bool Unlocked(string quest) { Get(quest); return recipes[quest].Requires.All((string x) => progress[x].Claimed); } public bool Complete(string quest) { return Get(quest).Counts.Select((int count, int index) => count >= recipes[quest].Objectives[index].Count).All((bool x) => x); } public bool CanClaim(string quest) { if (!Get(quest).Claimed && Unlocked(quest)) { return Complete(quest); } return false; } public void Record(string quest, int objectiveIndex, int amount) { PackQuestProgress packQuestProgress = Get(quest); if (objectiveIndex < 0 || objectiveIndex >= packQuestProgress.Counts.Count || amount < 1 || amount > 1000000) { throw new ArgumentOutOfRangeException("amount", "Invalid objective index or count."); } if (packQuestProgress.Claimed || !Unlocked(quest)) { throw new InvalidOperationException("Quest is claimed or locked: " + quest); } packQuestProgress.Counts[objectiveIndex] = (int)Math.Min((long)packQuestProgress.Counts[objectiveIndex] + (long)amount, recipes[quest].Objectives[objectiveIndex].Count); } public bool TryMarkClaimed(string quest) { if (!CanClaim(quest)) { return false; } Get(quest).Claimed = true; return true; } private PackQuestProgress Get(string quest) { if (quest == null || !progress.TryGetValue(quest, out PackQuestProgress value)) { throw new InvalidDataException("Unknown quest: " + quest); } return value; } } public sealed class PackRegistryOptions { public bool RequireSupportedExtensionHooks { get; set; } = true; public List SupportedExtensionHooks { get; set; } = new List(); } public sealed class ResolvedContent { public string PackKey { get; } public string Key { get; } public ContentDomain Domain { get; } public bool Native => PackKey == "native"; public int? Id { get; } public string Reference => PackKey + ":" + Key; internal ResolvedContent(string pack, string key, ContentDomain domain, int? id) { PackKey = pack; Key = key; Domain = domain; Id = id; } } public sealed class PackRegistry { public const string HarborPackKey = "gamblers_reach"; public const string HarborWildlifePackKey = "gamblers_reach_wildlife"; private readonly PackDefinition[] packs; private readonly Dictionary byKey; private readonly Dictionary references; public string Fingerprint { get; } public IReadOnlyList Packs => Array.AsReadOnly(packs.Select(PackCanonical.Copy).ToArray()); public IReadOnlyList Content { get; } private PackRegistry(PackDefinition[] ordered) { packs = ordered; byKey = packs.ToDictionary((PackDefinition x) => x.Key, StringComparer.Ordinal); references = new Dictionary(StringComparer.Ordinal); PackDefinition[] array = packs; foreach (PackDefinition packDefinition in array) { foreach (var item in PackValidation.Recipes(packDefinition)) { references.Add(packDefinition.Key + ":" + item.Recipe.Key, new ResolvedContent(packDefinition.Key, item.Recipe.Key, item.Domain, item.Id)); } } Content = Array.AsReadOnly(references.Values.OrderBy((ResolvedContent x) => x.Reference, StringComparer.Ordinal).ToArray()); Fingerprint = PackCanonical.Hash(packs.Select((PackDefinition x) => new SavedPack { Key = x.Key, Version = x.Version, Fingerprint = PackCanonical.Hash(x) }).ToList()); } public static PackRegistry Build(IEnumerable definitions, PackRegistryOptions? options = null) { if (definitions == null) { throw new ArgumentNullException("definitions"); } PackDefinition[] array = definitions.Take(65).ToArray(); if (array.Length > 64) { throw new InvalidDataException("At most 64 packs can be composed."); } if (options == null) { options = new PackRegistryOptions(); } if (options.SupportedExtensionHooks == null || options.SupportedExtensionHooks.Count > 64 || options.SupportedExtensionHooks.Any((string x) => !PackValidation.Key(x))) { throw new InvalidDataException("Invalid supported extension hooks."); } List list = new List(); PackDefinition[] array2 = array; foreach (PackDefinition packDefinition in array2) { if (packDefinition == null) { list.Add("Pack set contains null."); } else { list.AddRange(packDefinition.Inspect()); } } PackValidation.ThrowIfInvalid(list); PackDefinition[] array3 = array.Select(PackCanonical.Copy).ToArray(); if (array3.Select((PackDefinition x) => x.Key).Distinct(StringComparer.Ordinal).Count() != array3.Length) { throw new InvalidDataException("Duplicate pack keys."); } Dictionary dictionary = array3.ToDictionary((PackDefinition x) => x.Key, StringComparer.Ordinal); array2 = array3; foreach (PackDefinition packDefinition2 in array2) { foreach (PackDependency dependency in packDefinition2.Dependencies) { if (!dictionary.TryGetValue(dependency.Key, out var value)) { list.Add(packDefinition2.Key + ": missing required pack " + dependency.Key + "."); } else if ((dependency.ExactVersion != null) ? (dependency.ExactVersion != value.Version) : (PackValidation.CompareVersion(value.Version, dependency.MinimumVersion) < 0)) { list.Add(packDefinition2.Key + ": dependency version mismatch for " + dependency.Key + "."); } } if (!options.RequireSupportedExtensionHooks) { continue; } foreach (string extensionHook in packDefinition2.ExtensionHooks) { if (!options.SupportedExtensionHooks.Contains(extensionHook)) { list.Add(packDefinition2.Key + ": runtime does not support declared extension hook " + extensionHook + "."); } } } PackValidation.ThrowIfInvalid(list); PackRegistry packRegistry = new PackRegistry(Topological(array3, (PackDefinition p) => p.Key, (PackDefinition p) => p.Dependencies.Select((PackDependency x) => x.Key), "pack dependency").ToArray()); packRegistry.ValidateSet(list); PackValidation.ThrowIfInvalid(list); return packRegistry; } public ResolvedContent Resolve(string ownerPackKey, string reference, ContentDomain domain) { if (!byKey.TryGetValue(ownerPackKey, out PackDefinition value)) { throw new InvalidDataException("Unknown owner pack: " + ownerPackKey); } if (!PackValidation.TryReference(reference, out string targetPack, out string key)) { throw new InvalidDataException(ownerPackKey + ": malformed reference " + reference + "."); } if (targetPack == "native") { int num = int.Parse(key, CultureInfo.InvariantCulture); if (!NativePackCatalog.Contains(domain, num)) { throw new InvalidDataException("Unknown native " + domain.ToString() + " reference: " + reference); } return new ResolvedContent("native", key, domain, num); } if (targetPack != ownerPackKey && !value.Dependencies.Any((PackDependency x) => x.Key == targetPack)) { throw new InvalidDataException(ownerPackKey + ": reference needs explicit dependency on " + targetPack + "."); } if (!references.TryGetValue(reference, out ResolvedContent value2)) { throw new InvalidDataException(ownerPackKey + ": unresolved " + domain.ToString() + " reference " + reference + "."); } if (value2.Domain != domain) { throw new InvalidDataException(reference + ": expected " + domain.ToString() + ", found " + value2.Domain.ToString() + "."); } return value2; } public PackRegistryManifest CreateManifest() { PackRegistryManifest packRegistryManifest = new PackRegistryManifest { Fingerprint = Fingerprint, FingerprintVersion = 2 }; foreach (PackDefinition item in packs.OrderBy((PackDefinition x) => x.Key, StringComparer.Ordinal)) { packRegistryManifest.Packs.Add(new SavedPack { Key = item.Key, Version = item.Version, Fingerprint = PackCanonical.Hash(item) }); } foreach (ResolvedContent item2 in Content) { PackRecipe packRecipe = Recipe(item2); NetworkBinding networkBinding = ((packRecipe is ItemRecipe itemRecipe) ? itemRecipe.Network : ((packRecipe is NpcRecipe npcRecipe) ? npcRecipe.Network : null)); packRegistryManifest.Reservations.Add(new SavedReservation { PackKey = item2.PackKey, Key = item2.Key, Domain = item2.Domain, Id = item2.Id, CollectionId = networkBinding?.CollectionId, Slot = networkBinding?.Slot }); } foreach (PackDefinition item3 in packs.OrderBy((PackDefinition x) => x.Key, StringComparer.Ordinal)) { foreach (NetworkCollectionRecipe item4 in item3.Collections.OrderBy((NetworkCollectionRecipe x) => x.Key, StringComparer.Ordinal)) { packRegistryManifest.Collections.Add(new SavedCollection { PackKey = item3.Key, Key = item4.Key, Id = item4.Id, Capacity = item4.Capacity }); } } return packRegistryManifest; } public IReadOnlyList CompareManifest(PackRegistryManifest saved) { return PackManifestPolicy.Compare(saved, CreateManifest(), null, UnchangedLegacy); } public void RequireCompatible(PackRegistryManifest saved) { PackValidation.ThrowIfInvalid(CompareManifest(saved)); } public void RequireCompatible(PackRegistryManifest saved, Func acceptRevision) { PackValidation.ThrowIfInvalid(PackManifestPolicy.Compare(saved, CreateManifest(), acceptRevision ?? throw new ArgumentNullException("acceptRevision"), UnchangedLegacy)); } private bool UnchangedLegacy(SavedPack saved) { if (byKey.TryGetValue(saved.Key, out PackDefinition value) && value.Version == saved.Version) { return PackCanonical.LegacyHashes(value).Contains(saved.Fingerprint, StringComparer.Ordinal); } return false; } private PackRecipe Recipe(ResolvedContent content) { return PackValidation.Recipes(byKey[content.PackKey]).First<(ContentDomain, PackRecipe, int?)>(((ContentDomain Domain, PackRecipe Recipe, int? Id) x) => x.Recipe.Key == content.Key).Item2; } private void ValidateSet(List errors) { HashSet hashSet = new HashSet(StringComparer.Ordinal); HashSet hashSet2 = new HashSet(); Dictionary> dictionary = new Dictionary>(StringComparer.Ordinal); HashSet placements = new HashSet(StringComparer.Ordinal); HashSet hashSet3 = new HashSet(StringComparer.Ordinal); Dictionary assets = new Dictionary(StringComparer.OrdinalIgnoreCase); List<(string Pack, IslandRecipe Island)> routes = new List<(string, IslandRecipe)>(); PackDefinition[] array = packs; foreach (PackDefinition pack in array) { foreach (var item4 in PackValidation.Recipes(pack)) { if (item4.Id.HasValue) { ContentDomain item = item4.Domain; string text = item.ToString(); int? item2 = item4.Id; if (!hashSet.Add(text + ":" + item2)) { List list = errors; string[] obj = new string[5] { "Duplicate ", null, null, null, null }; (item, _, _) = item4; obj[1] = item.ToString(); obj[2] = " ID "; item2 = item4.Id; obj[3] = item2.ToString(); obj[4] = "."; list.Add(string.Concat(obj)); } } if (item4.Id.HasValue && IsHarborId(item4.Domain, item4.Id.Value) && pack.Key != "gamblers_reach") { List list2 = errors; string[] obj2 = new string[6] { pack.Key, ": ", null, null, null, null }; var (item, _, _) = item4; obj2[2] = item.ToString(); obj2[3] = " ID "; int? item2 = item4.Id; obj2[4] = item2.ToString(); obj2[5] = " is reserved for gamblers_reach."; list2.Add(string.Concat(obj2)); } if (item4.Domain == ContentDomain.Item && item4.Id == 196 && pack.Key != "gamblers_reach_wildlife") { errors.Add(pack.Key + ": item ID 196 is reserved for gamblers_reach_wildlife."); } ArtRecipe artRecipe = ((item4.Recipe is ItemRecipe itemRecipe) ? itemRecipe.Art : ((item4.Recipe is LureRecipe lureRecipe) ? lureRecipe.Art : ((item4.Recipe is NpcRecipe npcRecipe) ? npcRecipe.Art : null))); if (artRecipe != null) { Asset(pack.Key, artRecipe.Bundle, artRecipe.Sha256); } } foreach (NetworkCollectionRecipe collection in pack.Collections) { if (!hashSet2.Add(collection.Id)) { errors.Add("Duplicate network collection ID " + collection.Id + "."); } if ((collection.Id == 48187 || collection.Id == 48188 || collection.Id == 48189) && pack.Key != "gamblers_reach") { errors.Add(pack.Key + ": collection " + collection.Id + " is reserved for gamblers_reach."); } if (collection.Id == 48190 && pack.Key != "gamblers_reach_wildlife") { errors.Add(pack.Key + ": collection 48190 is reserved for gamblers_reach_wildlife."); } } foreach (ItemRecipe item3 in pack.Items) { if (item3.NativeDonor != null) { Guard(delegate { int value2 = Resolve(pack.Key, item3.NativeDonor, ContentDomain.Item).Id.Value; if (!NativePackCatalog.SupportsItemKind(value2, item3.Kind)) { throw new InvalidDataException(pack.Key + ":" + item3.Key + ": native donor does not support " + item3.Kind.ToString() + "."); } if (item3.Implementation != RecipeImplementation.External && !NativePackCatalog.IsCreature(value2) && (item3.Health.HasValue || item3.HealthRestored.HasValue || item3.FoodValue.HasValue || item3.BodyDamageFactor.HasValue || item3.CrewHealthFactor.HasValue || item3.CrewDamageFactor.HasValue)) { throw new InvalidDataException(item3.Key + ": health, food and creature damage overrides require a creature donor or an External hook."); } if (item3.Weapon != null && !NativePackCatalog.IsRangedWeapon(value2) && item3.Implementation != RecipeImplementation.External) { throw new InvalidDataException(item3.Key + ": ranged weapon policies require a ranged donor or an External hook."); } }); } if (item3.CookedItem != null) { Guard(delegate { Resolve(pack.Key, item3.CookedItem, ContentDomain.Item); }); } if (item3.Weapon?.AllowedAttachmentIds == null) { continue; } foreach (byte allowedAttachmentId in item3.Weapon.AllowedAttachmentIds) { if (!NativePackCatalog.Contains(ContentDomain.Attachment, allowedAttachmentId)) { errors.Add(item3.Key + ": unknown native attachment " + allowedAttachmentId + "."); } } } foreach (LureRecipe lure in pack.Lures) { if (lure.NativeVisualDonor != null) { Guard(delegate { Resolve(pack.Key, lure.NativeVisualDonor, ContentDomain.Lure); }); } foreach (CatchRecipe entry in lure.Catches) { Guard(delegate { Creature(pack.Key, entry.Item); }); } } foreach (CatchTablePatch patch in pack.CatchPatches) { Guard(delegate { Resolve(pack.Key, patch.TargetLure, ContentDomain.Lure); }); foreach (CatchRecipe entry2 in patch.Catches) { Guard(delegate { Creature(pack.Key, entry2.Item); }); } if (!dictionary.TryGetValue(patch.TargetLure, out var value)) { dictionary.Add(patch.TargetLure, value = new List()); } value.Add(patch.Mode); } foreach (NpcRecipe npc in pack.Npcs) { Guard(delegate { Place(pack.Key, npc.Island, npc.Placement); }); if (npc.NativeDonor != null && npc.NativeDonorIsland != null) { Guard(delegate { int value2 = Resolve(pack.Key, npc.NativeDonor, ContentDomain.Npc).Id.Value; if (!NativePackCatalog.HasNpcDonor(Resolve(pack.Key, npc.NativeDonorIsland, ContentDomain.Island).Id.Value, value2)) { throw new InvalidDataException(npc.Key + ": NPC donor is absent from the specified native island."); } }); } if (npc.Quest != null) { Guard(delegate { Resolve(pack.Key, npc.Quest, ContentDomain.Quest); }); } } foreach (ShopRecipe shop in pack.Shops) { Guard(delegate { Place(pack.Key, shop.Island, shop.Placement); }); Guard(delegate { Resolve(pack.Key, shop.Content, ShopDomain(shop.Kind)); }); } foreach (IslandRecipe island in pack.Islands) { if (!hashSet3.Add(island.SceneName)) { errors.Add("Duplicate scene name " + island.SceneName + "."); } Asset(pack.Key, island.SceneBundle, island.SceneSha256); if (island.PlayerSpawn.Marker != null && island.PlayerSpawn.Marker == island.BoatSpawn.Marker) { errors.Add(island.Key + ": player and boat spawn markers must differ."); } if (island.Route == null) { continue; } Guard(delegate { ResolvedContent resolvedContent = Resolve(pack.Key, island.Route.AfterIsland, ContentDomain.Island); ResolvedContent resolvedContent2 = Resolve(pack.Key, island.Route.BeforeIsland, ContentDomain.Island); if (resolvedContent.Reference == pack.Key + ":" + island.Key || resolvedContent2.Reference == pack.Key + ":" + island.Key) { throw new InvalidDataException(island.Key + ": island cannot route to itself."); } if (!resolvedContent.Native || !resolvedContent2.Native || resolvedContent2.Id != resolvedContent.Id + 1) { throw new InvalidDataException(island.Key + ": routes must identify a consecutive native island gap."); } routes.Add((pack.Key, island)); }); } foreach (QuestRecipe quest in pack.Quests) { foreach (string requirement in quest.Requires) { Guard(delegate { Resolve(pack.Key, requirement, ContentDomain.Quest); }); } foreach (QuestObjectiveRecipe objective in quest.Objectives) { Guard(delegate { Resolve(pack.Key, objective.Item, ContentDomain.Item); if (objective.Kind == QuestObjectiveKind.CatchItem) { Creature(pack.Key, objective.Item); } }); } foreach (QuestRewardRecipe reward in quest.Rewards) { if (reward.Kind != QuestRewardKind.Money) { Guard(delegate { Resolve(pack.Key, reward.Content, (reward.Kind != QuestRewardKind.Item) ? ((reward.Kind == QuestRewardKind.Lure) ? ContentDomain.Lure : ContentDomain.Island) : ContentDomain.Item); }); } } } foreach (LootRecipe loot in pack.Loot) { Guard(delegate { Creature(pack.Key, loot.Creature); }); foreach (LootDropRecipe drop in loot.Drops) { Guard(delegate { Resolve(pack.Key, drop.Item, ContentDomain.Item); }); } } } foreach (KeyValuePair> item5 in dictionary) { if (item5.Value.Contains(CatchPatchMode.ExclusiveReplace) && item5.Value.Count != 1) { errors.Add("Exclusive catch-table patch collision at " + item5.Key + "."); } } foreach (IGrouping item6 in from x in routes group x by x.Island.Route.AfterIsland + ">" + x.Island.Route.BeforeIsland) { (string, IslandRecipe)[] array2 = item6.OrderBy<(string, IslandRecipe), int>(((string Pack, IslandRecipe Island) x) => x.Island.Route.Priority).ThenBy<(string, IslandRecipe), string>(((string Pack, IslandRecipe Island) x) => x.Pack, StringComparer.Ordinal).ToArray(); for (int num = 1; num < array2.Length; num++) { if (array2[num - 1].Item2.Route.Priority == array2[num].Item2.Route.Priority || (array2[num - 1].Item1 != array2[num].Item1 && !DependsOn(array2[num].Item1, array2[num - 1].Item1))) { errors.Add("Shared route gap " + item6.Key + " needs distinct priorities and a dependency on the preceding pack."); } } } if (errors.Count != 0) { return; } Guard(delegate { Topological(packs.SelectMany((PackDefinition p) => p.Quests.Select((QuestRecipe q) => (Key: p.Key + ":" + q.Key, Quest: q))).ToArray(), ((string Key, QuestRecipe Quest) x) => x.Key, ((string Key, QuestRecipe Quest) x) => x.Quest.Requires, "quest dependency").ToArray(); }); void Asset(string owner, string bundle, string hash) { if (assets.TryGetValue(bundle, out (string, string) value2) && (value2.Item1 != owner || value2.Item2 != hash)) { errors.Add(bundle + ": bundle names must have one pack owner and a consistent hash."); } else { assets[bundle] = (owner, hash); } } void Guard(Action action) { try { action(); } catch (InvalidDataException ex) { if (errors.Count < 512) { errors.Add(ex.Message); } } } void Place(string owner, string islandRef, PlacementRecipe placement) { ResolvedContent resolvedContent = Resolve(owner, islandRef, ContentDomain.Island); if (placement.Marker != null) { if (resolvedContent.Native) { throw new InvalidDataException("Native-island additions must use explicit local poses, not unverified authored markers."); } if (!placements.Add(resolvedContent.Reference + ":" + placement.Marker)) { throw new InvalidDataException("Duplicate authored placement marker: " + placement.Marker); } } } } private void Creature(string owner, string reference) { ResolvedContent resolvedContent = Resolve(owner, reference, ContentDomain.Item); bool num; if (!resolvedContent.Native) { if (!(Recipe(resolvedContent) is ItemRecipe itemRecipe)) { goto IL_0051; } if (itemRecipe.Kind == PackItemKind.Fish) { return; } num = itemRecipe.Kind == PackItemKind.Creature; } else { num = NativePackCatalog.IsCreature(resolvedContent.Id.Value); } if (num) { return; } goto IL_0051; IL_0051: throw new InvalidDataException(reference + ": a fish/creature reference is required."); } private bool DependsOn(string owner, string target) { HashSet hashSet = new HashSet(StringComparer.Ordinal); Stack stack = new Stack(); stack.Push(owner); while (stack.Count != 0) { string text = stack.Pop(); if (!hashSet.Add(text)) { continue; } foreach (PackDependency dependency in byKey[text].Dependencies) { if (dependency.Key == target) { return true; } stack.Push(dependency.Key); } } return false; } public static ContentDomain ShopDomain(PackShopKind kind) { return kind switch { PackShopKind.Item => ContentDomain.Item, PackShopKind.Lure => ContentDomain.Lure, PackShopKind.Attachment => ContentDomain.Attachment, PackShopKind.Ammunition => ContentDomain.Ammunition, PackShopKind.BoatRadar => ContentDomain.BoatRadar, PackShopKind.Sharpening => ContentDomain.Sharpening, PackShopKind.Pocket => ContentDomain.Pocket, PackShopKind.Motor => ContentDomain.Motor, _ => throw new InvalidDataException("Unknown shop kind."), }; } public static bool IsHarborId(ContentDomain domain, int id) { return domain switch { ContentDomain.Item => (id >= 180 && id <= 185) || id == 187 || id == 188 || (id >= 190 && id <= 194), ContentDomain.Lure => id >= 17 && id <= 19, ContentDomain.Npc => id >= 230 && id <= 232, ContentDomain.Island => id == 6, _ => false, }; } private static IEnumerable Topological(IEnumerable values, Func key, Func> dependencies, string label) { Dictionary remaining = values.ToDictionary(key, StringComparer.Ordinal); HashSet done = new HashSet(StringComparer.Ordinal); while (remaining.Count > 0) { string[] array = remaining.Keys.Where((string x) => dependencies(remaining[x]).All(done.Contains)).OrderBy((string x) => x, StringComparer.Ordinal).ToArray(); if (array.Length == 0) { throw new InvalidDataException("Cycle or missing " + label + ": " + string.Join(", ", remaining.Keys.OrderBy((string x) => x, StringComparer.Ordinal))); } string[] array2 = array; foreach (string text in array2) { T val = remaining[text]; remaining.Remove(text); done.Add(text); yield return val; } } } } public sealed class PackRegistryManifest { public int SchemaVersion { get; set; } = 1; public int FingerprintVersion { get; set; } = 1; public string GameBuild { get; set; } = "25127368"; public string Fingerprint { get; set; } = ""; public List Packs { get; set; } = new List(); public List Reservations { get; set; } = new List(); public List Collections { get; set; } = new List(); } public sealed class SavedPack { public string Key { get; set; } = ""; public string Version { get; set; } = ""; public string Fingerprint { get; set; } = ""; } public sealed class SavedReservation { public string PackKey { get; set; } = ""; public string Key { get; set; } = ""; public ContentDomain Domain { get; set; } public int? Id { get; set; } public ushort? CollectionId { get; set; } public ushort? Slot { get; set; } } public sealed class SavedCollection { public string PackKey { get; set; } = ""; public string Key { get; set; } = ""; public ushort Id { get; set; } public int Capacity { get; set; } } internal static class PackManifestPolicy { internal static IReadOnlyList Compare(PackRegistryManifest saved, PackRegistryManifest current, Func? acceptRevision = null, Func? unchangedLegacy = null) { List list = new List(); bool flag = saved == null || saved.SchemaVersion != 1; if (!flag) { int fingerprintVersion = saved.FingerprintVersion; bool flag2 = ((fingerprintVersion < 1 || fingerprintVersion > 2) ? true : false); flag = flag2; } if (flag || saved.GameBuild != "25127368" || saved.Packs == null || saved.Packs.Count > 64 || saved.Packs.Any((SavedPack x) => x == null) || saved.Reservations == null || saved.Reservations.Count > 65536 || saved.Reservations.Any((SavedReservation x) => x == null) || saved.Collections == null || saved.Collections.Count > 2048 || saved.Collections.Any((SavedCollection x) => x == null)) { list.Add("Saved pack registry schema, game build or bounded collections are invalid."); return list.AsReadOnly(); } if (saved.Packs.Any((SavedPack x) => !PackValidation.Key(x.Key) || !PackValidation.Version(x.Version) || !PackValidation.Hash(x.Fingerprint)) || saved.Packs.Select((SavedPack x) => x.Key).Distinct(StringComparer.Ordinal).Count() != saved.Packs.Count) { list.Add("Saved pack identities are invalid or duplicated."); return list.AsReadOnly(); } bool flag3 = ((saved.FingerprintVersion == 2) ? (saved.Fingerprint == PackCanonical.Hash(saved.Packs)) : PackCanonical.LegacyHashes(saved.Packs).Contains(saved.Fingerprint, StringComparer.Ordinal)); if (!PackValidation.Hash(saved.Fingerprint) || !flag3) { list.Add("Saved registry fingerprint does not match its pack metadata."); } HashSet owners = new HashSet(saved.Packs.Select((SavedPack x) => x.Key), StringComparer.Ordinal); bool num = saved.Reservations.Any((SavedReservation x) => !owners.Contains(x.PackKey) || !PackValidation.Key(x.Key) || !Enum.IsDefined(typeof(ContentDomain), x.Domain) || x.Id < 0 || x.Id > 255 || x.CollectionId.HasValue != x.Slot.HasValue); bool flag4 = saved.Collections.Any((SavedCollection x) => !owners.Contains(x.PackKey) || !PackValidation.Key(x.Key) || x.Id < 48000 || x.Capacity < 1 || x.Capacity > 65536); if (num || flag4 || saved.Reservations.Select(Identity).Distinct(StringComparer.Ordinal).Count() != saved.Reservations.Count || saved.Collections.Select((SavedCollection x) => x.PackKey + ":" + x.Key).Distinct(StringComparer.Ordinal).Count() != saved.Collections.Count || saved.Collections.Select((SavedCollection x) => x.Id).Distinct().Count() != saved.Collections.Count) { list.Add("Saved reservations/collections are invalid or duplicated."); return list.AsReadOnly(); } if (list.Count != 0) { return list.AsReadOnly(); } foreach (SavedPack pack in saved.Packs) { if (current.Packs.FirstOrDefault((SavedPack x) => x.Key == pack.Key) == null) { list.Add("Missing required saved pack " + pack.Key + " " + pack.Version + "."); } } Dictionary dictionary = current.Reservations.ToDictionary(Identity, StringComparer.Ordinal); foreach (SavedReservation reservation in saved.Reservations) { if (!dictionary.TryGetValue(Identity(reservation), out var value)) { list.Add("Missing saved content " + Identity(reservation) + "."); } else if (reservation.Domain != value.Domain || reservation.Id != value.Id || reservation.CollectionId != value.CollectionId || reservation.Slot != value.Slot) { list.Add("Saved ID/collection slot remapped: " + Identity(reservation) + "."); } } Dictionary dictionary2 = current.Collections.ToDictionary((SavedCollection x) => x.PackKey + ":" + x.Key, StringComparer.Ordinal); foreach (SavedCollection collection in saved.Collections) { if (!dictionary2.TryGetValue(collection.PackKey + ":" + collection.Key, out var value2) || collection.Id != value2.Id || collection.Capacity != value2.Capacity) { list.Add("Saved collection missing or remapped: " + collection.PackKey + ":" + collection.Key + "."); } } if (current.Reservations.Count((SavedReservation x) => owners.Contains(x.PackKey)) != saved.Reservations.Count || current.Collections.Count((SavedCollection x) => owners.Contains(x.PackKey)) != saved.Collections.Count) { list.Add("Saved registry content/reservation inventory differs from its required packs."); } if (list.Count != 0) { return list.AsReadOnly(); } foreach (SavedPack pack2 in saved.Packs) { SavedPack savedPack = current.Packs.Single((SavedPack x) => x.Key == pack2.Key); if ((!(savedPack.Version == pack2.Version) || (!(savedPack.Fingerprint == pack2.Fingerprint) && (saved.FingerprintVersion != 1 || unchangedLegacy == null || !unchangedLegacy(pack2)))) && (acceptRevision == null || !acceptRevision(new SavedPack { Key = pack2.Key, Version = pack2.Version, Fingerprint = pack2.Fingerprint }, new SavedPack { Key = savedPack.Key, Version = savedPack.Version, Fingerprint = savedPack.Fingerprint }))) { list.Add("Changed required saved pack " + pack2.Key + ": version or gameplay/assets fingerprint differs."); } } return list.AsReadOnly(); } private static string Identity(SavedReservation entry) { return entry.PackKey + ":" + entry.Key; } } internal static class PackValidation { private sealed class Inspector { private readonly string pack; internal readonly List Errors = new List(); internal Inspector(string pack) { this.pack = pack; } internal void Require(bool condition, string error) { if (!condition && Errors.Count < 256) { Errors.Add(pack + ": " + error); } } internal void Text(string? text, int max, string label) { Require(!string.IsNullOrWhiteSpace(text) && text.Length <= max && !text.Contains("\0"), "Invalid " + label + "."); } internal bool List(List? list, int max, string label) { bool flag = list != null && list.Count <= max && list.All((T x) => x != null); Require(flag, label + " must be a non-null bounded list without null entries (maximum " + max + ")."); return flag; } internal void Unique(IEnumerable values, string label) { Require(values.Distinct().Count() == values.Count(), "Duplicate " + label + "."); } internal void Enum(T value, string label) where T : struct { Require(System.Enum.IsDefined(typeof(T), value), "Invalid " + label + "."); } internal void Integer(int? value, int min, int max, string label) { Require(!value.HasValue || (value >= min && value <= max), label + " out of bounds."); } internal void Number(float? value, float min, float max, string label) { Require(!value.HasValue || (!float.IsNaN(value.Value) && !float.IsInfinity(value.Value) && value >= min && value <= max), label + " is not finite or out of bounds."); } internal void Reference(string? reference, string label) { Require(TryReference(reference, out string _, out string _), "Invalid " + label + " reference."); } internal void Donor(PackRecipe recipe, string? donor) { if (recipe.Implementation == RecipeImplementation.NativeClone || donor != null) { Require(NativeRef(donor), recipe.Key + ": a native donor reference is required."); } } internal void Binding(NetworkBinding? binding, List collections, List used, string label) { if (binding == null) { Require(condition: false, label + ": network binding required."); return; } Require(collections.Any((NetworkCollectionRecipe x) => x.Id == binding.CollectionId && binding.Slot < x.Capacity), label + ": network binding is outside declared collection capacity."); used.Add(binding.CollectionId + ":" + binding.Slot); } internal void File(string? value, string label) { Require(value != null && value.Length <= 128 && Regex.IsMatch(value, "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$") && !value.Contains(".."), "Invalid local " + label + "."); } internal void Name(string? value, string label) { Require(value != null && value.Length <= 128 && Regex.IsMatch(value, "^[a-zA-Z0-9_][a-zA-Z0-9_ .-]*$") && !value.Contains(".."), "Invalid " + label + "."); } internal void Vector(VectorRecipe? vector, bool scale) { if (vector == null) { Require(condition: false, "Vector cannot be null."); return; } float min = (scale ? 0.0001f : (-100000f)); float max = (scale ? 1000 : 100000); Number(vector.X, min, max, "vector X"); Number(vector.Y, min, max, "vector Y"); Number(vector.Z, min, max, "vector Z"); } private void Pose(PoseRecipe pose) { Vector(pose.Position, scale: false); Number(pose.Yaw, -360f, 360f, "yaw"); } internal void Placement(PlacementRecipe? placement) { if (placement == null) { Require(condition: false, "Placement required."); return; } Require(placement.Marker == null != (placement.Pose == null), "Placement must specify exactly one marker or local pose."); if (placement.Marker != null) { Name(placement.Marker, "marker"); } if (placement.Pose != null) { Pose(placement.Pose); } } internal void Art(ArtRecipe? art) { if (art != null) { File(art.Bundle, "art bundle"); Name(art.Prefab, "prefab"); Require(Hash(art.Sha256), "Art bundle SHA256 must be lowercase hexadecimal."); Vector(art.Position, scale: false); Vector(art.Rotation, scale: false); Vector(art.Scale, scale: true); if (art.Grip != null) { Pose(art.Grip); } } } internal void Scales(List? values, string label) { if (!List(values, 32, label)) { return; } foreach (float value in values) { Number(value, 0.001f, 1000f, label); } } internal void Weapon(WeaponRecipe weapon) { Number(weapon.DamageScale, 0.001f, 1000f, "weapon damage"); Scales(weapon.UpgradeDamageScales, "damage upgrades"); Enum(weapon.MagazinePolicy, "magazine policy"); Enum(weapon.ReloadPolicy, "reload policy"); Integer(weapon.MagazineSize, 1, 10000, "magazine size"); if (List(weapon.UpgradeMagazineSizes, 32, "magazine upgrades")) { foreach (int upgradeMagazineSize in weapon.UpgradeMagazineSizes) { Integer(upgradeMagazineSize, 1, 10000, "upgrade magazine"); } } Require((weapon.MagazinePolicy == MagazinePolicy.Fixed) ? weapon.MagazineSize.HasValue : (!weapon.MagazineSize.HasValue), "Fixed magazine policy requires size; other policies must omit size."); Require(weapon.UpgradeMagazineSizes != null && ((weapon.MagazinePolicy == MagazinePolicy.PerUpgrade) ? (weapon.UpgradeMagazineSizes.Count > 0) : (weapon.UpgradeMagazineSizes.Count == 0)), "PerUpgrade magazine policy requires upgrade sizes exclusively."); Number(weapon.ReloadSeconds, 0.001f, 3600f, "reload time"); Number(weapon.ShotDelaySeconds, 0.001f, 3600f, "shot delay"); if (weapon.AllowedAttachmentIds != null && List(weapon.AllowedAttachmentIds, 32, "attachments")) { Unique(weapon.AllowedAttachmentIds, "attachments"); } } internal void Catches(List? catches, string key) { if (!List(catches, 128, "catches")) { return; } Require(catches.Count > 0, key + ": catch table cannot be empty."); Unique(catches.Select((CatchRecipe x) => x.Item), "catch items"); foreach (CatchRecipe @catch in catches) { Reference(@catch.Item, "catch item"); Number(@catch.Weight, 1E-06f, 1000000f, "catch weight"); } } } internal const int MaxPacks = 64; internal static bool Key(string? value) { if (value != null && value.Length <= 64) { return Regex.IsMatch(value, "^[a-z][a-z0-9_-]*$", RegexOptions.CultureInvariant); } return false; } internal static bool Version(string? value) { if (value != null && value.Length <= 32) { return Regex.IsMatch(value, "^(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})\\.(0|[1-9][0-9]{0,5})$", RegexOptions.CultureInvariant); } return false; } internal static int CompareVersion(string a, string b) { int[] array = a.Split('.').Select(int.Parse).ToArray(); int[] array2 = b.Split('.').Select(int.Parse).ToArray(); for (int i = 0; i < 3; i++) { if (array[i] != array2[i]) { return array[i].CompareTo(array2[i]); } } return 0; } internal static bool Hash(string? value) { if (value != null && value.Length == 64) { return value.All((char c) => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); } return false; } internal static bool NativeRef(string? value) { if (TryReference(value, out string owner, out string _)) { return owner == "native"; } return false; } internal static bool TryReference(string? value, out string owner, out string key) { owner = ""; key = ""; if (value == null || value.Length > 130) { return false; } string[] array = value.Split(':'); if (array.Length != 2) { return false; } owner = array[0]; key = array[1]; if (owner != "native") { if (Key(owner)) { return Key(key); } return false; } if (byte.TryParse(key, out var result)) { return key == result.ToString(CultureInfo.InvariantCulture); } return false; } internal static void ThrowIfInvalid(IReadOnlyList errors) { if (errors.Count > 0) { throw new InvalidDataException(string.Join(Environment.NewLine, errors)); } } internal static IReadOnlyList Inspect(PackDefinition pack) { Inspector inspector = new Inspector(pack.Key); inspector.Require(pack.SchemaVersion == 1, "Unsupported pack schema."); inspector.Require(Key(pack.Key) && pack.Key != "native", "Invalid or reserved pack key."); inspector.Text(pack.Title, 160, "title"); inspector.Require(Version(pack.Version), "Version must be canonical major.minor.patch."); inspector.Require(pack.GameBuild == "25127368", "Unsupported native game build."); if (!(inspector.List(pack.Dependencies, 64, "dependencies") & inspector.List(pack.ExtensionHooks, 64, "hooks") & inspector.List(pack.Collections, 32, "collections") & inspector.List(pack.Items, 128, "items") & inspector.List(pack.Lures, 64, "lures") & inspector.List(pack.CatchPatches, 64, "catch patches") & inspector.List(pack.Npcs, 64, "NPCs") & inspector.List(pack.Shops, 128, "shops") & inspector.List(pack.Islands, 32, "islands") & inspector.List(pack.Quests, 128, "quests") & inspector.List(pack.Loot, 128, "loot"))) { return inspector.Errors.AsReadOnly(); } inspector.Unique(pack.Dependencies.Select((PackDependency x) => x.Key), "dependency keys"); foreach (PackDependency dependency in pack.Dependencies) { inspector.Require(Key(dependency.Key) && dependency.Key != pack.Key && dependency.Key != "native", "Invalid dependency key."); inspector.Require(dependency.MinimumVersion == null != (dependency.ExactVersion == null), "Each dependency needs exactly one minimum or exact version."); inspector.Require(Version(dependency.MinimumVersion ?? dependency.ExactVersion), "Invalid dependency version."); } inspector.Unique(pack.ExtensionHooks, "extension hooks"); foreach (string extensionHook in pack.ExtensionHooks) { inspector.Require(Key(extensionHook), "Invalid extension hook."); } PackRecipe[] array = (from x in Recipes(pack) select x.Recipe).Concat(pack.CatchPatches).ToArray(); inspector.Unique(array.Select((PackRecipe x) => x.Key), "local recipe keys (across all domains)"); PackRecipe[] array2 = array; foreach (PackRecipe packRecipe in array2) { inspector.Require(Key(packRecipe.Key), "Invalid recipe key."); inspector.Enum(packRecipe.Implementation, packRecipe.Key + " implementation"); if (packRecipe.Implementation == RecipeImplementation.External) { inspector.Require(packRecipe.ExtensionHook != null && pack.ExtensionHooks.Contains(packRecipe.ExtensionHook), packRecipe.Key + ": External requires a declared extension hook."); } else { inspector.Require(packRecipe.ExtensionHook == null, packRecipe.Key + ": native clone cannot specify an extension hook."); } } inspector.Unique(pack.Collections.Select((NetworkCollectionRecipe x) => x.Key), "collection keys"); inspector.Unique(pack.Collections.Select((NetworkCollectionRecipe x) => x.Id), "collection IDs"); foreach (NetworkCollectionRecipe collection in pack.Collections) { inspector.Require(Key(collection.Key) && collection.Id >= 48000 && collection.Capacity >= 1 && collection.Capacity <= 65536, "Collections need a key, reserved ushort ID >=48000 and capacity 1..65536."); } inspector.Unique(pack.Items.Select((ItemRecipe x) => x.Id), "item IDs"); inspector.Unique(pack.Lures.Select((LureRecipe x) => x.Id), "lure IDs"); inspector.Unique(pack.Npcs.Select((NpcRecipe x) => x.Id), "NPC IDs"); inspector.Unique(pack.Islands.Select((IslandRecipe x) => x.Id), "island IDs"); List list = new List(); foreach (ItemRecipe item in pack.Items) { inspector.Text(item.Title, 160, item.Key + " title"); inspector.Enum(item.Kind, item.Key + " kind"); inspector.Require(item.Id >= 86, item.Key + ": custom item ID overlaps native table."); inspector.Donor(item, item.NativeDonor); inspector.Binding(item.Network, pack.Collections, list, item.Key); inspector.Art(item.Art); inspector.Integer(item.Worth, 0, 100000000, item.Key + " worth"); inspector.Integer(item.Cost, 0, 100000000, item.Key + " cost"); inspector.Number(item.Health, 0.001f, 100000000f, item.Key + " health"); inspector.Number(item.HealthRestored, 0f, 1000000f, item.Key + " health restored"); inspector.Number(item.FoodValue, 0f, 1000000f, item.Key + " food"); inspector.Number(item.BodyDamageFactor, 0f, 1000f, item.Key + " body damage factor"); inspector.Number(item.CrewHealthFactor, 0f, 1000f, item.Key + " crew health factor"); inspector.Number(item.CrewDamageFactor, 0f, 1000f, item.Key + " crew damage factor"); if (item.CookedItem != null) { inspector.Reference(item.CookedItem, item.Key + " cooked item"); } inspector.Require(item.Weapon == null || item.Kind == PackItemKind.Weapon, item.Key + ": weapon policy on nonweapon."); inspector.Require(item.Rod == null || item.Kind == PackItemKind.Rod, item.Key + ": rod policy on nonrod."); if (item.Weapon != null) { inspector.Weapon(item.Weapon); } if (item.Rod != null) { inspector.Number(item.Rod.MaximumLineLength, 0.001f, 10000f, "rod maximum line length"); inspector.Number(item.Rod.LineStrengthScale, 0.001f, 1000f, "rod line scale"); inspector.Number(item.Rod.ReelingSpeedScale, 0.001f, 1000f, "rod reeling scale"); inspector.Scales(item.Rod.UpgradeLineStrengthScales, "rod line upgrades"); inspector.Scales(item.Rod.UpgradeReelingSpeedScales, "rod reeling upgrades"); } } foreach (LureRecipe lure in pack.Lures) { inspector.Require(lure.Id >= 17 && lure.Id < byte.MaxValue, lure.Key + ": custom lure IDs must be 17..254."); inspector.Text(lure.Title, 160, lure.Key + " title"); inspector.Text(lure.Description, 2048, lure.Key + " description"); inspector.Donor(lure, lure.NativeVisualDonor); inspector.Art(lure.Art); inspector.Integer(lure.Cost, 0, 100000000, "lure cost"); inspector.Number(lure.LossPercent, 0f, 100f, "lure loss"); inspector.Number(lure.CatchTimeMin, 0.001f, 86400f, "lure minimum time"); inspector.Number(lure.CatchTimeMax, 0.001f, 86400f, "lure maximum time"); inspector.Require(lure.CatchTimeMin.HasValue == lure.CatchTimeMax.HasValue && (!lure.CatchTimeMin.HasValue || lure.CatchTimeMax >= lure.CatchTimeMin), lure.Key + ": override both catch times, with maximum >= minimum."); inspector.Catches(lure.Catches, lure.Key); } foreach (CatchTablePatch catchPatch in pack.CatchPatches) { inspector.Reference(catchPatch.TargetLure, "target lure"); inspector.Enum(catchPatch.Mode, "catch patch mode"); inspector.Catches(catchPatch.Catches, catchPatch.Key); } foreach (NpcRecipe npc in pack.Npcs) { inspector.Require(npc.Id >= 200 && npc.Id < byte.MaxValue, npc.Key + ": custom NPC IDs must be 200..254; 255 is a native sentinel."); inspector.Text(npc.Title, 160, "NPC title"); inspector.Donor(npc, npc.NativeDonor); inspector.Require(npc.NativeDonor == null == (npc.NativeDonorIsland == null), npc.Key + ": NPC donor island and ID must be supplied together."); if (npc.Implementation == RecipeImplementation.NativeClone || npc.NativeDonorIsland != null) { inspector.Require(NativeRef(npc.NativeDonorIsland), npc.Key + ": NPC donor island must be native."); } if (npc.Implementation == RecipeImplementation.NativeClone) { inspector.Require(npc.Network == null, npc.Key + ": native NPCs use stable RPC IDs, not network prefab bindings; omit Network."); } else if (npc.Network != null) { inspector.Binding(npc.Network, pack.Collections, list, npc.Key); } inspector.Reference(npc.Island, "NPC island"); inspector.Placement(npc.Placement); inspector.Enum(npc.Role, "NPC role"); inspector.Art(npc.Art); if (inspector.List(npc.Dialogue, 32, "dialogue")) { inspector.Require(npc.Implementation != RecipeImplementation.NativeClone || npc.Dialogue.Count > 0, npc.Key + ": native NPC original dialogue is required."); foreach (string item2 in npc.Dialogue) { inspector.Text(item2, 2048, "dialogue"); } } inspector.Require(npc.Role != PackNpcRole.External || npc.Implementation == RecipeImplementation.External, npc.Key + ": External role needs an External hook."); inspector.Require(npc.Role != PackNpcRole.DeliveryQuest || npc.Quest != null, npc.Key + ": quest role requires a quest."); if (npc.Quest != null) { inspector.Reference(npc.Quest, "NPC quest"); } } inspector.Unique(list, "network collection slots"); foreach (ShopRecipe shop in pack.Shops) { inspector.Reference(shop.Island, "shop island"); inspector.Placement(shop.Placement); inspector.Enum(shop.Kind, "shop type"); inspector.Reference(shop.Content, "shop content"); inspector.Integer(shop.Cost, 0, 100000000, "shop cost"); inspector.Enum(shop.CapPolicy, "shop cap policy"); inspector.Require((shop.CapPolicy == ShopCapPolicy.Unlimited) ? (!shop.Cap.HasValue) : (shop.Cap >= 1 && shop.Cap <= 1000000), shop.Key + ": capped shops require positive cap; unlimited shops must omit cap."); } foreach (IslandRecipe island in pack.Islands) { inspector.Require(island.Id >= 5 && island.Id < byte.MaxValue, island.Key + ": custom island engine IDs must be 5..254; 0..4 are native and 255 is the unload sentinel."); inspector.Text(island.Title, 160, "island title"); inspector.File(island.SceneBundle, "scene bundle"); inspector.Require(Hash(island.SceneSha256), island.Key + ": scene bundle SHA256 must be lowercase hexadecimal."); inspector.Name(island.SceneName, "scene name"); inspector.Name(island.SceneRoot, "scene root"); inspector.Require(island.UnlockThreshold >= 0 && island.UnlockThreshold <= 5, "Unlock threshold must be 0..5 native islands."); inspector.Placement(island.PlayerSpawn); inspector.Placement(island.BoatSpawn); inspector.Vector(island.WorldMapPosition, scale: false); if (island.Route != null) { inspector.Reference(island.Route.AfterIsland, "route after island"); inspector.Reference(island.Route.BeforeIsland, "route before island"); inspector.Require(island.Route.AfterIsland != island.Route.BeforeIsland, "Route endpoints must differ."); inspector.Require(island.Route.Priority >= -1000000 && island.Route.Priority <= 1000000, "Route priority out of bounds."); } } foreach (QuestRecipe quest in pack.Quests) { inspector.Text(quest.Title, 160, "quest title"); if (inspector.List(quest.Requires, 128, "quest dependencies")) { inspector.Unique(quest.Requires, "quest dependencies"); foreach (string require in quest.Requires) { inspector.Reference(require, "quest dependency"); } } if (inspector.List(quest.Objectives, 32, "quest objectives")) { inspector.Require(quest.Objectives.Count > 0, "Quest needs an objective."); foreach (QuestObjectiveRecipe objective in quest.Objectives) { inspector.Enum(objective.Kind, "objective kind"); inspector.Reference(objective.Item, "objective item"); inspector.Require(objective.Count >= 1 && objective.Count <= 1000000, "Objective count out of bounds."); } } if (!inspector.List(quest.Rewards, 32, "quest rewards")) { continue; } inspector.Require(quest.Rewards.Count > 0, "Quest needs a reward."); foreach (QuestRewardRecipe reward in quest.Rewards) { inspector.Enum(reward.Kind, "reward kind"); inspector.Require(reward.Amount >= 1 && reward.Amount <= 100000000, "Reward amount out of bounds."); if (reward.Kind == QuestRewardKind.Item) { inspector.Require(reward.Amount <= 1000, "Item reward amount cannot exceed 1000."); } if (reward.Kind == QuestRewardKind.Lure) { inspector.Require(reward.Amount == 1, "A lure reward unlocks one lure entry."); } if (reward.Kind == QuestRewardKind.Money) { inspector.Require(reward.Content == null, "Money reward must omit content."); } else { inspector.Reference(reward.Content, "reward content"); } if (reward.Kind == QuestRewardKind.Coordinates) { inspector.Require(reward.Amount == 1, "Coordinates reward amount must be one."); } } } foreach (LootRecipe item3 in pack.Loot) { inspector.Reference(item3.Creature, "loot creature"); if (!inspector.List(item3.Drops, 64, "loot drops")) { continue; } inspector.Require(item3.Drops.Count > 0, "Loot needs a drop."); inspector.Unique(item3.Drops.Select((LootDropRecipe x) => x.Item), "loot drop items"); foreach (LootDropRecipe drop in item3.Drops) { inspector.Reference(drop.Item, "loot drop item"); inspector.Require(drop.FixedQuantity >= 0 && drop.FixedQuantity <= 10000 && drop.PerPlayerQuantity >= 0 && drop.PerPlayerQuantity <= 10000 && drop.FixedQuantity + drop.PerPlayerQuantity > 0, "Loot quantities out of bounds."); } } return inspector.Errors.AsReadOnly(); } internal static IEnumerable<(ContentDomain Domain, PackRecipe Recipe, int? Id)> Recipes(PackDefinition pack) { foreach (ItemRecipe item in pack.Items) { yield return (Domain: ContentDomain.Item, Recipe: item, Id: item.Id); } foreach (LureRecipe lure in pack.Lures) { yield return (Domain: ContentDomain.Lure, Recipe: lure, Id: lure.Id); } foreach (NpcRecipe npc in pack.Npcs) { yield return (Domain: ContentDomain.Npc, Recipe: npc, Id: npc.Id); } foreach (IslandRecipe island in pack.Islands) { yield return (Domain: ContentDomain.Island, Recipe: island, Id: island.Id); } foreach (QuestRecipe quest in pack.Quests) { yield return (Domain: ContentDomain.Quest, Recipe: quest, Id: null); } foreach (ShopRecipe shop in pack.Shops) { yield return (Domain: ContentDomain.Shop, Recipe: shop, Id: null); } foreach (LootRecipe item2 in pack.Loot) { yield return (Domain: ContentDomain.Loot, Recipe: item2, Id: null); } } } } namespace HowToFish.ExpansionKit.Balance { public readonly struct CatchBudgetEntry { public float Weight { get; } public int Worth { get; } public bool Sellable { get; } public int Health { get; } public bool ScalesWithCrew { get; } public float AdditionalCrewHealth { get; } public CatchBudgetEntry(float weight, int worth, bool sellable, int health, bool scalesWithCrew = false, float additionalCrewHealth = 0.5f) { if (float.IsNaN(weight) || float.IsInfinity(weight) || weight <= 0f) { throw new ArgumentOutOfRangeException("weight"); } if (worth < 0) { throw new ArgumentOutOfRangeException("worth"); } if (health < 0 || (scalesWithCrew && health == 0)) { throw new ArgumentOutOfRangeException("health"); } if (float.IsNaN(additionalCrewHealth) || float.IsInfinity(additionalCrewHealth) || additionalCrewHealth < 0f) { throw new ArgumentOutOfRangeException("additionalCrewHealth"); } Weight = weight; Worth = worth; Sellable = sellable; Health = health; ScalesWithCrew = scalesWithCrew; AdditionalCrewHealth = additionalCrewHealth; } } public sealed class CatchPoolBudget { public double WeightedSaleValue { get; } public double LureLossBudget { get; } public double NeutralMargin => WeightedSaleValue - LureLossBudget; public double WeightedHealth { get; } public double WeightedIdealTriggerPulls { get; } private CatchPoolBudget(double sale, double loss, double health, double triggers) { WeightedSaleValue = sale; LureLossBudget = loss; WeightedHealth = health; WeightedIdealTriggerPulls = triggers; } public static CatchPoolBudget Evaluate(IReadOnlyList entries, int lureCost, float lossPercent, int crewCount, int damagePerProjectile, int projectilesPerTrigger = 1, float healthDifficulty = 1f) { if (entries == null) { throw new ArgumentNullException("entries"); } if (entries.Count == 0 || entries.Count > 256) { throw new ArgumentOutOfRangeException("entries"); } if (lureCost < 0) { throw new ArgumentOutOfRangeException("lureCost"); } if (float.IsNaN(lossPercent) || float.IsInfinity(lossPercent) || lossPercent < 0f || lossPercent > 100f) { throw new ArgumentOutOfRangeException("lossPercent"); } if (crewCount < 1) { throw new ArgumentOutOfRangeException("crewCount"); } if (float.IsNaN(healthDifficulty) || float.IsInfinity(healthDifficulty) || healthDifficulty <= 0f) { throw new ArgumentOutOfRangeException("healthDifficulty"); } NativeBalance.IdealTriggerPulls(1, damagePerProjectile, projectilesPerTrigger); double num = 0.0; foreach (CatchBudgetEntry entry in entries) { if (entry.Weight <= 0f) { throw new ArgumentException("The catch pool contains an uninitialized entry.", "entries"); } num += (double)entry.Weight; } if (num > 3.4028234663852886E+38) { throw new ArgumentOutOfRangeException("entries"); } double num2 = 0.0; double num3 = 0.0; double num4 = 0.0; foreach (CatchBudgetEntry entry2 in entries) { double num5 = (double)entry2.Weight / num; if (entry2.Sellable) { num2 += num5 * (double)NativeBalance.ItemWorth(entry2.Worth, 1f, 1f, 1f, 1f); } if (entry2.Health != 0) { int num6 = NativeBalance.BossHealth(entry2.Health, healthDifficulty, (!entry2.ScalesWithCrew) ? 1 : crewCount, entry2.AdditionalCrewHealth); num3 += num5 * (double)num6; num4 += num5 * (double)NativeBalance.IdealTriggerPulls(num6, damagePerProjectile, projectilesPerTrigger); } } return new CatchPoolBudget(num2, (double)lureCost * (double)lossPercent / 100.0, num3, num4); } } public static class NativeBalance { public const float DefaultBossHealthPerAdditionalCrew = 0.5f; public const float DefaultBossDamagePerAdditionalCrew = 0.2f; public const int DefaultNpcProjectileDamagePerAdditionalCrew = 2; public const float ExplosionCreatureHealthBonus = 0.05f; public static int CreatureHealth(int authoredHealth, float difficultyHealthMultiplier) { RequirePositive(authoredHealth, "authoredHealth"); RequireFinitePositive(difficultyHealthMultiplier, "difficultyHealthMultiplier"); return TruncatePositive((float)authoredHealth * difficultyHealthMultiplier, "difficultyHealthMultiplier"); } public static int BossHealth(int authoredHealth, float difficultyHealthMultiplier, int crewCount, float additionalCrewMultiplier = 0.5f) { return ScalePerCrew(CreatureHealth(authoredHealth, difficultyHealthMultiplier), crewCount, additionalCrewMultiplier, "additionalCrewMultiplier"); } public static int BossCollisionDamage(int baseDamage, int crewCount, float additionalCrewMultiplier = 0.2f) { return ScalePerCrew(baseDamage, crewCount, additionalCrewMultiplier, "additionalCrewMultiplier"); } public static int NpcProjectileDamage(int baseDamage, int crewCount, int damagePerAdditionalCrew = 2) { RequirePositive(baseDamage, "baseDamage"); RequireCrew(crewCount); if (damagePerAdditionalCrew < 0) { throw new ArgumentOutOfRangeException("damagePerAdditionalCrew"); } return checked(baseDamage + (crewCount - 1) * damagePerAdditionalCrew); } public static int IdealPelletHits(int targetHealth, int damagePerPellet) { RequirePositive(targetHealth, "targetHealth"); RequirePositive(damagePerPellet, "damagePerPellet"); return 1 + (targetHealth - 1) / damagePerPellet; } public static int ExplosionDamageOnCreature(int baseDamage, int authoredHealth, float difficultyHealthMultiplier) { if (baseDamage < 0) { throw new ArgumentOutOfRangeException("baseDamage"); } int num = TruncateNonNegative((float)CreatureHealth(authoredHealth, difficultyHealthMultiplier) * 0.05f, "authoredHealth"); return checked(baseDamage + num); } public static int IdealTriggerPulls(int targetHealth, int damagePerPellet, int pelletsPerTrigger) { RequirePositive(targetHealth, "targetHealth"); RequirePositive(damagePerPellet, "damagePerPellet"); RequirePositive(pelletsPerTrigger, "pelletsPerTrigger"); int damagePerPellet2 = checked(damagePerPellet * pelletsPerTrigger); return IdealPelletHits(targetHealth, damagePerPellet2); } public static int ItemWorth(int authoredWorth, float randomizedWeight, float evaluatedCookingMultiplier, float bettingMultiplier, float killScoreMultiplier) { if (authoredWorth < 0) { throw new ArgumentOutOfRangeException("authoredWorth"); } RequireFiniteNonNegative(randomizedWeight, "randomizedWeight"); RequireFiniteNonNegative(evaluatedCookingMultiplier, "evaluatedCookingMultiplier"); RequireFiniteNonNegative(bettingMultiplier, "bettingMultiplier"); RequireFiniteNonNegative(killScoreMultiplier, "killScoreMultiplier"); return TruncateNonNegative((float)TruncateNonNegative((float)authoredWorth * randomizedWeight * evaluatedCookingMultiplier * bettingMultiplier, "bettingMultiplier") * killScoreMultiplier, "killScoreMultiplier"); } private static int ScalePerCrew(int baseValue, int crewCount, float multiplier, string multiplierName) { RequirePositive(baseValue, "baseValue"); RequireCrew(crewCount); RequireFiniteNonNegative(multiplier, multiplierName); checked { int num = TruncateNonNegative((float)(baseValue * (crewCount - 1)) * multiplier, multiplierName); return baseValue + num; } } private static int TruncatePositive(float value, string parameterName) { int num = TruncateNonNegative(value, parameterName); if (num <= 0) { throw new ArgumentOutOfRangeException(parameterName, "The native truncation produced no health."); } return num; } private static int TruncateNonNegative(float value, string parameterName) { if (float.IsNaN(value) || float.IsInfinity(value) || value < 0f || (double)value > 2147483647.0) { throw new ArgumentOutOfRangeException(parameterName, "The native Single-precision result is outside Int32 range."); } return (int)value; } private static void RequireCrew(int crewCount) { if (crewCount < 1) { throw new ArgumentOutOfRangeException("crewCount"); } } private static void RequirePositive(int value, string name) { if (value <= 0) { throw new ArgumentOutOfRangeException(name); } } private static void RequireFinitePositive(float value, string name) { if (float.IsNaN(value) || float.IsInfinity(value) || value <= 0f) { throw new ArgumentOutOfRangeException(name); } } private static void RequireFiniteNonNegative(float value, string name) { if (float.IsNaN(value) || float.IsInfinity(value) || value < 0f) { throw new ArgumentOutOfRangeException(name); } } } } namespace HowToFish.ExpansionKit.Assets { public sealed class AuthoredAsset { public int SchemaVersion { get; set; } public string Key { get; set; } = ""; public string Units { get; set; } = ""; public List Materials { get; set; } = new List(); public List Parts { get; set; } = new List(); public Dictionary Markers { get; set; } = new Dictionary(); public void Validate(string expectedKey) { if (SchemaVersion != 1 || Key != expectedKey || Units != "meters") { throw new InvalidDataException("The authored asset has an incompatible identity, schema or unit scale: " + expectedKey); } if (Materials == null || Materials.Count == 0 || Materials.Count > 64 || Parts == null || Parts.Count == 0 || Parts.Count > 512 || Markers == null) { throw new InvalidDataException("The authored asset has no valid material or mesh collection: " + Key); } foreach (AuthoredMaterial material in Materials) { if (material == null || string.IsNullOrWhiteSpace(material.Name) || !Vector(material.Color, 4) || material.Color.Any((float value) => value < 0f || value > 1f) || !Fraction(material.Metallic) || !Fraction(material.Smoothness)) { throw new InvalidDataException("The authored asset contains an invalid material: " + Key); } } HashSet hashSet = new HashSet(StringComparer.Ordinal); int num = 0; foreach (AuthoredPart part in Parts) { if (part == null || string.IsNullOrWhiteSpace(part.Name) || !hashSet.Add(part.Name) || (part.Role != "body" && part.Role != "detail") || !new string[6] { "root", "reel", "crank", "slew", "bolt", "magazine" }.Contains(part.Anchor) || part.Material < 0 || part.Material >= Materials.Count) { throw new InvalidDataException("The authored asset contains an invalid part: " + Key); } if (part.Vertices == null || part.Normals == null || part.Uvs == null || part.Triangles == null || part.Vertices.Length < 3 || part.Vertices.Length > 200000 || part.Normals.Length != part.Vertices.Length || part.Uvs.Length != part.Vertices.Length || part.Triangles.Length < 3 || part.Triangles.Length % 3 != 0 || part.Triangles.Length > 600000) { throw new InvalidDataException("The authored mesh has inconsistent attribute counts: " + part.Name); } num += part.Vertices.Length; for (int num2 = 0; num2 < part.Vertices.Length; num2++) { if (!Vector(part.Vertices[num2], 3) || part.Vertices[num2].Any((float value) => Math.Abs(value) > 100f) || !Vector(part.Normals[num2], 3) || !Vector(part.Uvs[num2], 2)) { throw new InvalidDataException("The authored mesh has invalid coordinates: " + part.Name); } float num3 = part.Normals[num2].Sum((float value) => value * value); if (num3 < 0.98f || num3 > 1.02f) { throw new InvalidDataException("The authored mesh has a non-unit normal: " + part.Name); } } if (part.Triangles.Any((int index) => index < 0 || index >= part.Vertices.Length)) { throw new InvalidDataException("The authored mesh has an out-of-range triangle: " + part.Name); } for (int num4 = 0; num4 < part.Triangles.Length; num4 += 3) { float[] array = part.Vertices[part.Triangles[num4]]; float[] array2 = part.Vertices[part.Triangles[num4 + 1]]; float[] array3 = part.Vertices[part.Triangles[num4 + 2]]; double num5 = (array2[1] - array[1]) * (array3[2] - array[2]) - (array2[2] - array[2]) * (array3[1] - array[1]); double num6 = (array2[2] - array[2]) * (array3[0] - array[0]) - (array2[0] - array[0]) * (array3[2] - array[2]); double num7 = (array2[0] - array[0]) * (array3[1] - array[1]) - (array2[1] - array[1]) * (array3[0] - array[0]); if (num5 * num5 + num6 * num6 + num7 * num7 < 1E-20) { throw new InvalidDataException("The authored mesh has a degenerate triangle: " + part.Name); } } } if (num > 500000) { throw new InvalidDataException("The authored asset exceeds its geometry budget: " + Key); } foreach (KeyValuePair marker in Markers) { if (string.IsNullOrWhiteSpace(marker.Key) || !Vector(marker.Value, 3)) { throw new InvalidDataException("The authored asset has an invalid attachment marker: " + Key); } } if ((Key.EndsWith("_perch", StringComparison.Ordinal) || Key.EndsWith("_bream", StringComparison.Ordinal) || Key.EndsWith("_eel", StringComparison.Ordinal) || Key.EndsWith("_puffer", StringComparison.Ordinal) || Key.EndsWith("_ray", StringComparison.Ordinal) || Key.EndsWith("_koi", StringComparison.Ordinal) || Key == "barracuda" || Key == "deepsea_angler") && !Parts.Any((AuthoredPart authoredPart) => authoredPart.Role == "body" && authoredPart.Name.EndsWith("sculpted body", StringComparison.Ordinal))) { throw new InvalidDataException("The fish is missing its anatomical collision body: " + Key); } } private static bool Fraction(float value) { if (Finite(value) && value >= 0f) { return value <= 1f; } return false; } private static bool Vector(float[] value, int count) { if (value != null && value.Length == count) { return value.All(Finite); } return false; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } public sealed class AuthoredMaterial { public string Name { get; set; } = ""; public float[] Color { get; set; } = Array.Empty(); public float Metallic { get; set; } public float Smoothness { get; set; } } public sealed class AuthoredPart { public string Name { get; set; } = ""; public string Role { get; set; } = ""; public string Anchor { get; set; } = ""; public float[][] Vertices { get; set; } = Array.Empty(); public float[][] Normals { get; set; } = Array.Empty(); public float[][] Uvs { get; set; } = Array.Empty(); public int[] Triangles { get; set; } = Array.Empty(); public int Material { get; set; } } public sealed class SurfaceOverlap { public string FirstPart { get; } public int FirstTriangle { get; } public string SecondPart { get; } public int SecondTriangle { get; } public double Area { get; } public double Separation { get; } internal SurfaceOverlap(string firstPart, int firstTriangle, string secondPart, int secondTriangle, double area, double separation) { FirstPart = firstPart; FirstTriangle = firstTriangle; SecondPart = secondPart; SecondTriangle = secondTriangle; Area = area; Separation = separation; } } public static class MeshSurfaceAudit { private readonly struct Point2 { internal readonly double X; internal readonly double Y; internal Point2(double x, double y) { X = x; Y = y; } } private sealed class Triangle { internal readonly string Part; internal readonly int Index; internal readonly Vector3 A; internal readonly Vector3 B; internal readonly Vector3 C; internal readonly Vector3 Min; internal readonly Vector3 Max; internal readonly Vector3 Normal; internal Triangle(string part, int index, Vector3 a, Vector3 b, Vector3 c) { Part = part; Index = index; A = a; B = b; C = c; Min = Vector3.Min(a, Vector3.Min(b, c)); Max = Vector3.Max(a, Vector3.Max(b, c)); Vector3 value = Vector3.Cross(b - a, c - a); float num = value.LengthSquared(); if (!Finite(num) || num < 1E-20f) { throw new InvalidDataException("Surface audit received an invalid or degenerate triangle in " + part + "."); } Normal = Vector3.Normalize(value); } } public const float DefaultMaximumSeparation = 0.0005f; public const double DefaultMinimumArea = 1E-06; public static IReadOnlyList FindCoplanarOverlaps(IReadOnlyList parts, float maximumSeparation = 0.0005f, double minimumArea = 1E-06, int maximumResults = 64) { if (parts == null) { throw new ArgumentNullException("parts"); } if (!Finite(maximumSeparation) || maximumSeparation < 0f || double.IsNaN(minimumArea) || double.IsInfinity(minimumArea) || minimumArea <= 0.0 || maximumResults < 1) { throw new ArgumentOutOfRangeException("maximumSeparation"); } List list = new List(); foreach (AuthoredPart part in parts) { if (part == null || part.Vertices == null || part.Triangles == null || part.Triangles.Length % 3 != 0) { throw new InvalidDataException("Surface auditing requires valid triangulated parts."); } Vector3[] array = part.Vertices.Select(Point).ToArray(); for (int i = 0; i < part.Triangles.Length; i += 3) { int num = part.Triangles[i]; int num2 = part.Triangles[i + 1]; int num3 = part.Triangles[i + 2]; if (num < 0 || num2 < 0 || num3 < 0 || num >= array.Length || num2 >= array.Length || num3 >= array.Length) { throw new InvalidDataException("Surface audit triangle index is outside " + part.Name + "."); } list.Add(new Triangle(part.Name, i / 3, array[num], array[num2], array[num3])); } } list.Sort((Triangle a, Triangle b) => a.Min.X.CompareTo(b.Min.X)); List list2 = new List(); for (int num4 = 0; num4 < list.Count; num4++) { Triangle triangle = list[num4]; for (int num5 = num4 + 1; num5 < list.Count; num5++) { Triangle triangle2 = list[num5]; if (triangle2.Min.X > triangle.Max.X + maximumSeparation) { break; } if (triangle2.Min.Y > triangle.Max.Y + maximumSeparation || triangle.Min.Y > triangle2.Max.Y + maximumSeparation || triangle2.Min.Z > triangle.Max.Z + maximumSeparation || triangle.Min.Z > triangle2.Max.Z + maximumSeparation || Vector3.Dot(triangle.Normal, triangle2.Normal) < 0.99999f) { continue; } double num6 = Math.Max(Math.Abs(Vector3.Dot(triangle2.A - triangle.A, triangle.Normal)), Math.Max(Math.Abs(Vector3.Dot(triangle2.B - triangle.A, triangle.Normal)), Math.Abs(Vector3.Dot(triangle2.C - triangle.A, triangle.Normal)))); if (num6 > (double)maximumSeparation) { continue; } int axis = DominantAxis(triangle.Normal); double num7 = IntersectionArea(triangle, triangle2, axis) / (double)Math.Abs(Component(triangle.Normal, axis)); if (!(num7 <= minimumArea)) { list2.Add(new SurfaceOverlap(triangle.Part, triangle.Index, triangle2.Part, triangle2.Index, num7, num6)); if (list2.Count == maximumResults) { return list2.AsReadOnly(); } } } } return list2.AsReadOnly(); } public static void RequireClearSurfaces(AuthoredAsset asset, float maximumSeparation = 0.0005f) { if (asset == null) { throw new ArgumentNullException("asset"); } asset.Validate(asset.Key); IReadOnlyList readOnlyList = FindCoplanarOverlaps(asset.Parts, maximumSeparation); if (readOnlyList.Count != 0) { throw new InvalidDataException(asset.Key + ": overlapping render surfaces: " + string.Join("; ", from overlap in readOnlyList.Take(8) select $"{overlap.FirstPart}[{overlap.FirstTriangle}] / {overlap.SecondPart}[{overlap.SecondTriangle}] " + $"({overlap.Area:G4} m2, {overlap.Separation:G4} m apart)") + ((readOnlyList.Count > 8) ? "; additional overlaps omitted." : ".")); } } private static double IntersectionArea(Triangle a, Triangle b, int axis) { List list = new List { Project(a.A, axis), Project(a.B, axis), Project(a.C, axis) }; Point2[] array = new Point2[3] { Project(b.A, axis), Project(b.B, axis), Project(b.C, axis) }; if (Cross(array[0], array[1], array[2]) < 0.0) { Array.Reverse(array); } for (int i = 0; i < 3; i++) { if (list.Count <= 0) { break; } Point2 a2 = array[i]; Point2 b2 = array[(i + 1) % 3]; List list2 = new List(); Point2 c = list[list.Count - 1]; double num = Cross(a2, b2, c); foreach (Point2 item in list) { double num2 = Cross(a2, b2, item); bool num3 = num2 >= -1E-12; bool flag = num >= -1E-12; if (num3 != flag) { double num4 = num / (num - num2); list2.Add(new Point2(c.X + (item.X - c.X) * num4, c.Y + (item.Y - c.Y) * num4)); } if (num3) { list2.Add(item); } c = item; num = num2; } list = list2; } double num5 = 0.0; for (int j = 0; j < list.Count; j++) { Point2 point = list[j]; Point2 point2 = list[(j + 1) % list.Count]; num5 += point.X * point2.Y - point2.X * point.Y; } return Math.Abs(num5) * 0.5; } private static Vector3 Point(float[] point) { if (point == null || point.Length != 3 || point.Any((float value) => !Finite(value))) { throw new InvalidDataException("Surface audit coordinates must be finite three-component vectors."); } return new Vector3(point[0], point[1], point[2]); } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } private static int DominantAxis(Vector3 normal) { if (!(Math.Abs(normal.X) >= Math.Abs(normal.Y)) || !(Math.Abs(normal.X) >= Math.Abs(normal.Z))) { if (!(Math.Abs(normal.Y) >= Math.Abs(normal.Z))) { return 2; } return 1; } return 0; } private static float Component(Vector3 value, int axis) { return axis switch { 1 => value.Y, 0 => value.X, _ => value.Z, }; } private static Point2 Project(Vector3 point, int axis) { return axis switch { 1 => new Point2(point.X, point.Z), 0 => new Point2(point.Y, point.Z), _ => new Point2(point.X, point.Y), }; } private static double Cross(Point2 a, Point2 b, Point2 c) { return (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X); } } }