Иконка ресурса

FREE RustGeneticsHelper : 1.0.0 1.0.0

Нет прав для скачивания
C#:
// Reference: 0Harmony // (not required, kept for parity with common plugin templates; remove if your build warns)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("RustGeneticsHelper", "jakman76", "1.0.0")]
    [Description("Farming genetics assistant: clone bank scanner and a recipe finder that tells you which clones to place to reach a target genome.")]
    public class RustGeneticsHelper : RustPlugin
    {
        // ------------------------------------------------------------------
        //  GENETICS MODEL  (validated against the in-game crossbreed rules)
        // ------------------------------------------------------------------
        //  * 6 gene slots. 5 gene types:
        //        G GrowthSpeed  (green)   Y Yield (green)   H Hardiness (green)
        //        W WaterReq     (red)     X Empty (red)
        //  * Crossbreed resolves each of the 6 slots INDEPENDENTLY.
        //  * The plant being resolved is the "center" (defender). Its touching
        //    same-species neighbours in the same planter are the "donors".
        //  * Per slot, sum donor weights per gene type (green = 0.6, red = 1.0).
        //    The heaviest donor type is the challenger.
        //  * STRICT-GREATER rule: the challenger overwrites the center's gene
        //    ONLY IF challengerWeight > centerWeight. Ties/less => center keeps.
        //  * If several donor types tie for the heaviest AND beat the center,
        //    the winner is chosen uniformly at random -> that is where the
        //    probabilities in this plugin come from.
        //  All four numbers (green/red weight, strict-greater) are config-driven
        //  so you can adjust if Facepunch ever retunes the values.
        // ------------------------------------------------------------------

        private const int Slots = 6;
        private const string GreenBg = "0.15 0.36 0.14 0.95"; // unified green (title bar, presets, goal letters)

        #region Config

        private Configuration cfg;

        private class Configuration
        {
            [JsonProperty("Permission required to use")]
            public string Permission = "rustgeneticshelper.use";

            [JsonProperty("Permission for admin/testing commands (grow)")]
            public string AdminPermission = "rustgeneticshelper.admin";

            [JsonProperty("Green gene weight (G/Y/H)")]
            public float GreenWeight = 0.6f;

            [JsonProperty("Red gene weight (W/X)")]
            public float RedWeight = 1.0f;

            [JsonProperty("Default target genome (order-independent for stats)")]
            public string TargetGenome = "GGGYYY";

            [JsonProperty("Recipe finder: how many recipes to show")]
            public int FinderResults = 5;

            [JsonProperty("Scan boxes within this radius for clones (m)")]
            public float BoxScanRadius = 4f;

            [JsonProperty("Also scan teammates' boxes")]
            public bool ScanTeammateBoxes = true;

            [JsonProperty("Max distinct clones listed per page")]
            public int PanelCloneRows = 6;

            [JsonProperty("Target presets (name -> genome)")]
            public Dictionary<string, string> TargetPresets = new()
            {
                { "Growth",   "GGGGGG" },
                { "Yield",    "YYYYYY" },
                { "Balanced", "GGGYYY" },
                { "Hardy",    "GGYYHH" },
            };

            [JsonProperty("Look-at ray distance (m)")]
            public float RayDistance = 5f;

            [JsonProperty("Planter neighbour scan radius (m)")]
            public float PlanterScanRadius = 2.0f;

            [JsonProperty("Green colour (rich-text hex)")]
            public string GreenColorHex = "#7BFF52";

            [JsonProperty("Red colour (rich-text hex)")]
            public string RedColorHex = "#FF5B5B";
        }

        protected override void LoadDefaultConfig() => cfg = new Configuration();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                cfg = Config.ReadObject<Configuration>();
                if (cfg == null) throw new Exception("null config");
            }
            catch
            {
                PrintWarning("Config invalid or missing – creating a fresh one.");
                cfg = new Configuration();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(cfg);

        #endregion

        #region Gene helpers

        // Compile-time reference to the game's enum is safe – it is public.
        private static readonly Dictionary<GrowableGenetics.GeneType, char> TypeToChar = new()
        {
            { GrowableGenetics.GeneType.GrowthSpeed,      'G' },
            { GrowableGenetics.GeneType.Yield,            'Y' },
            { GrowableGenetics.GeneType.Hardiness,        'H' },
            { GrowableGenetics.GeneType.WaterRequirement, 'W' },
            { GrowableGenetics.GeneType.Empty,            'X' },
        };

        private static bool IsRed(char c) => c == 'W' || c == 'X';
        private static bool IsGreen(char c) => c == 'G' || c == 'Y' || c == 'H';
        private static bool IsValidGene(char c) => "GYHWX".IndexOf(c) >= 0;

        // Read the six gene letters off a live plant.
        private string ReadGenome(GrowableEntity plant) => plant == null ? null : ReadGenomeFromGenetics(plant.Genes);

        // Read genes from a GrowableGenetics object (from a plant OR a decoded item).
        // Uses reflection so an internal rename by Facepunch won't break the plugin.
        private string ReadGenomeFromGenetics(object genesObj)
        {
            if (genesObj == null) return null;

            IEnumerable<object> elements = ExtractGeneElements(genesObj);
            if (elements == null) return null;

            var sb = new StringBuilder(Slots);
            foreach (var el in elements)
            {
                if (el is GrowableGenetics.GeneType gt)
                {
                    if (TypeToChar.TryGetValue(gt, out var ch)) sb.Append(ch);
                }
                else
                {
                    object typeVal = GetMember(el, "Type") ?? FindMemberOfType(el, typeof(GrowableGenetics.GeneType));
                    if (typeVal is GrowableGenetics.GeneType g2 && TypeToChar.TryGetValue(g2, out var ch))
                        sb.Append(ch);
                }
                if (sb.Length >= Slots) break;
            }
            return sb.Length == Slots ? sb.ToString() : null;
        }

        private static IEnumerable<object> ExtractGeneElements(object genesObj)
        {
            const BindingFlags F = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

            // Try common member names first.
            foreach (var nm in new[] { "Genes", "genes", "Slots", "slots", "geneList" })
            {
                var l = ToGeneList(GetMember(genesObj, nm));
                if (LooksLikeGenes(l)) return l;
            }

            // Otherwise scan every enumerable member and keep the one that looks like genes.
            var t = genesObj.GetType();
            foreach (var f in t.GetFields(F))
            {
                if (!typeof(System.Collections.IEnumerable).IsAssignableFrom(f.FieldType) || f.FieldType == typeof(string)) continue;
                var l = ToGeneList(SafeGet(() => f.GetValue(genesObj)));
                if (LooksLikeGenes(l)) return l;
            }
            foreach (var p in t.GetProperties(F))
            {
                if (!p.CanRead || !typeof(System.Collections.IEnumerable).IsAssignableFrom(p.PropertyType) || p.PropertyType == typeof(string)) continue;
                if (p.GetIndexParameters().Length > 0) continue;
                var l = ToGeneList(SafeGet(() => p.GetValue(genesObj)));
                if (LooksLikeGenes(l)) return l;
            }
            return null;
        }

        private static object SafeGet(Func<object> f) { try { return f(); } catch { return null; } }

        private static List<object> ToGeneList(object o)
        {
            if (o is not System.Collections.IEnumerable e || o is string) return null;
            var list = new List<object>();
            foreach (var x in e) list.Add(x);
            return list;
        }

        // True if this enumerable is actually the 6 gene slots (each element is a
        // GeneType, or an object exposing a GeneType member).
        private static bool LooksLikeGenes(List<object> list)
        {
            if (list == null || list.Count < Slots) return false;
            var first = list[0];
            if (first is GrowableGenetics.GeneType) return true;
            return GetMember(first, "Type") is GrowableGenetics.GeneType
                   || FindMemberOfType(first, typeof(GrowableGenetics.GeneType)) is GrowableGenetics.GeneType;
        }

        private static object GetMember(object obj, string name)
        {
            if (obj == null) return null;
            var t = obj.GetType();
            var f = t.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (f != null) return f.GetValue(obj);
            var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            return p != null && p.CanRead ? p.GetValue(obj) : null;
        }

        private static object FindMemberOfType(object obj, Type wanted)
        {
            if (obj == null) return null;
            const BindingFlags F = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
            var t = obj.GetType();
            foreach (var f in t.GetFields(F))
                if (f.FieldType == wanted) return f.GetValue(obj);
            foreach (var p in t.GetProperties(F))
                if (p.PropertyType == wanted && p.CanRead && p.GetIndexParameters().Length == 0) return SafeGet(() => p.GetValue(obj));
            return null;
        }

        #endregion

        #region Neighbour detection

        private GrowableEntity GetLookedAtPlant(BasePlayer player)
        {
            if (player == null) return null;
            var ray = player.eyes.HeadRay();

            // Find the surface point the player is aiming at (terrain, planter, foundation…).
            Vector3 aim = ray.origin + ray.direction * Mathf.Min(cfg.RayDistance, 2f);
            var hits = Physics.RaycastAll(ray, cfg.RayDistance, ~0, QueryTriggerInteraction.Collide);
            if (hits != null && hits.Length > 0)
            {
                Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
                bool haveAim = false;
                foreach (var h in hits)
                {
                    var g = ResolveGrowable(h);
                    if (g != null) return g; // rare: growable with a collider

                    var child = NearestChildGrowableToRay(h.GetEntity(), ray, 0.5f);
                    if (child != null) return child; // planter-hosted plants

                    if (!haveAim) { aim = h.point; haveAim = true; }
                }
            }

            // Growables usually have NO server-side collider, so physics can't see them.
            // Query Rust's entity grid around the aim point and pick the plant the
            // crosshair is pointing at (smallest perpendicular distance to the ray).
            return NearestGrowableToRayGrid(ray, aim, 1.5f, 0.5f);
        }

        private static readonly BaseEntity[] _queryBuffer = new BaseEntity[256];

        private List<GrowableEntity> FindGrowablesNear(Vector3 pos, float radius)
        {
            var list = new List<GrowableEntity>();
            try
            {
                int count = BaseEntity.Query.Server.GetInSphere(pos, radius, _queryBuffer,
                    e => e is GrowableEntity ge && ge != null && !ge.IsDestroyed);
                for (int i = 0; i < count && i < _queryBuffer.Length; i++)
                    if (_queryBuffer[i] is GrowableEntity g) list.Add(g);
            }
            catch (Exception ex) { PrintWarning($"entity query failed: {ex.Message}"); }
            return list;
        }

        private GrowableEntity NearestGrowableToRayGrid(Ray ray, Vector3 center, float searchRadius, float maxPerp)
        {
            GrowableEntity best = null;
            float bestPerp = maxPerp;
            foreach (var g in FindGrowablesNear(center, searchRadius))
            {
                float perp = PerpDistToRay(ray, g.transform.position);
                if (perp < bestPerp) { bestPerp = perp; best = g; }
            }
            return best;
        }

        private static GrowableEntity ResolveGrowable(RaycastHit h)
        {
            var ent = h.GetEntity();
            if (ent is GrowableEntity g1) return g1;
            var g2 = h.collider.ToBaseEntity() as GrowableEntity;
            return g2 ?? h.collider.GetComponentInParent<GrowableEntity>();
        }

        // Nearest child GrowableEntity of 'host' to the aim ray (perpendicular
        // distance), within maxPerp metres. Handles plants parented to a planter.
        private static GrowableEntity NearestChildGrowableToRay(BaseEntity host, Ray ray, float maxPerp)
        {
            if (host?.children == null) return null;
            GrowableEntity best = null;
            float bestPerp = maxPerp;
            foreach (var ch in host.children)
            {
                if (ch is not GrowableEntity g) continue;
                float perp = PerpDistToRay(ray, g.transform.position);
                if (perp < bestPerp) { bestPerp = perp; best = g; }
            }
            return best;
        }

        private static float PerpDistToRay(Ray ray, Vector3 point)
        {
            Vector3 toPoint = point - ray.origin;
            if (Vector3.Dot(toPoint, ray.direction) < 0f) return float.MaxValue; // behind the player
            return Vector3.Cross(toPoint, ray.direction.normalized).magnitude;
        }

        #endregion

        #region Recipe finder (deterministic, allocation-free)

        // A one-round recipe: center + donor multiset, with success probability P.
        // AnyCenter = throwaway/sacrificial center (the donors dominate it).
        private class DetRecipe { public string Center; public List<string> Donors; public double P = 1.0; public bool AnyCenter; }

        // A two-round bridge path: round 1 breeds a clean bridge donor, round 2 uses
        // it (throwaway center both rounds) to finish the target.
        private class TwoStagePath
        {
            public string Bridge;
            public List<string> R1Donors;  // throwaway center -> bridge
            public List<string> R2Donors;  // throwaway center (+ bridge) -> target
            public double P;               // combined round-1 x round-2 probability
        }

        private float Wt(char c) => (c == 'W' || c == 'X') ? cfg.RedWeight : cfg.GreenWeight;
        private static bool Approx(float a, float b) => a > b - 1e-4f && a < b + 1e-4f;

        // Does the donor multiset make slot 's' deterministically equal 'want' (given the center)?
        // Allocation-free: five weight accumulators, no dictionaries or LINQ.
        private bool SlotYields(char center, List<string> donors, int s, char want)
        {
            float g = 0, y = 0, h = 0, w = 0, x = 0;
            for (int i = 0; i < donors.Count; i++)
            {
                char d = donors[i][s];
                float wt = Wt(d);
                if (d == 'G') g += wt; else if (d == 'Y') y += wt; else if (d == 'H') h += wt; else if (d == 'W') w += wt; else x += wt;
            }
            float max = g; char win = 'G';
            if (y > max) { max = y; win = 'Y'; }
            if (h > max) { max = h; win = 'H'; }
            if (w > max) { max = w; win = 'W'; }
            if (x > max) { max = x; win = 'X'; }

            float cw = Wt(center);
            if (max <= 0f || !(max > cw + 1e-4f)) return center == want; // center defends

            int ties = 0;
            if (Approx(g, max)) ties++; if (Approx(y, max)) ties++; if (Approx(h, max)) ties++;
            if (Approx(w, max)) ties++; if (Approx(x, max)) ties++;
            return ties == 1 && win == want; // unique winner, and it's what we want
        }

        private bool YieldsTarget(string center, List<string> donors, string target)
        {
            for (int s = 0; s < Slots; s++)
                if (!SlotYields(center[s], donors, s, target[s])) return false;
            return true;
        }

        // Probability that slot 's' ends up == 'want' (accounts for ties = coin flips).
        private double SlotP(char center, List<string> donors, int s, char want)
        {
            float g = 0, y = 0, h = 0, w = 0, x = 0;
            for (int i = 0; i < donors.Count; i++)
            {
                char d = donors[i][s];
                float wt = Wt(d);
                if (d == 'G') g += wt; else if (d == 'Y') y += wt; else if (d == 'H') h += wt; else if (d == 'W') w += wt; else x += wt;
            }
            float max = g;
            if (y > max) max = y; if (h > max) max = h; if (w > max) max = w; if (x > max) max = x;

            float cw = Wt(center);
            if (max <= 0f || !(max > cw + 1e-4f)) return center == want ? 1.0 : 0.0; // center defends

            int ties = 0;
            if (Approx(g, max)) ties++; if (Approx(y, max)) ties++; if (Approx(h, max)) ties++;
            if (Approx(w, max)) ties++; if (Approx(x, max)) ties++;
            float wantAcc = want == 'G' ? g : want == 'Y' ? y : want == 'H' ? h : want == 'W' ? w : x;
            return Approx(wantAcc, max) ? 1.0 / ties : 0.0;
        }

        private double GenomeP(string center, List<string> donors, string target)
        {
            double p = 1.0;
            for (int s = 0; s < Slots; s++)
            {
                p *= SlotP(center[s], donors, s, target[s]);
                if (p <= 0.0) return 0.0;
            }
            return p;
        }

        // With-replacement combinations of pool strings, up to maxK; onCombo returns false to abort.
        private bool EnumCombos(List<string> pool, int start, int maxK, List<string> cur, Func<List<string>, bool> onCombo)
        {
            if (cur.Count > 0 && !onCombo(cur)) return false;
            if (cur.Count == maxK) return true;
            for (int i = start; i < pool.Count; i++)
            {
                cur.Add(pool[i]);
                bool ok = EnumCombos(pool, i, maxK, cur, onCombo);
                cur.RemoveAt(cur.Count - 1);
                if (!ok) return false;
            }
            return true;
        }

        private static int WrongSlots(string a, string b)
        {
            int n = 0;
            for (int i = 0; i < Slots; i++) if (a[i] != b[i]) n++;
            return n;
        }

        // Find a clean donor multiset (2 copies of up to 3 bank clones) that turns
        // 'center' into 'target' deterministically. Returns the donor genomes or null.
        private List<string> TryFinish(string center, string target, List<string> bank)
        {
            var wrong = new List<int>();
            for (int s = 0; s < Slots; s++) if (center[s] != target[s]) wrong.Add(s);
            if (wrong.Count == 0) return new List<string>(); // already the target

            // Candidate donors: only clones that supply a needed target gene.
            var cand = new List<string>();
            foreach (var d in bank)
                if (wrong.Any(s => d[s] == target[s])) cand.Add(d);
            if (cand.Count == 0) return null;

            var donors = new List<string>(6);
            // size 1 (fewest donors first)
            for (int i = 0; i < cand.Count; i++)
            {
                donors.Clear(); donors.Add(cand[i]); donors.Add(cand[i]);
                if (YieldsTarget(center, donors, target)) return new List<string>(donors);
            }
            // size 2
            for (int i = 0; i < cand.Count; i++)
                for (int j = i + 1; j < cand.Count; j++)
                {
                    donors.Clear();
                    donors.Add(cand[i]); donors.Add(cand[i]); donors.Add(cand[j]); donors.Add(cand[j]);
                    if (YieldsTarget(center, donors, target)) return new List<string>(donors);
                }
            // size 3
            for (int i = 0; i < cand.Count; i++)
                for (int j = i + 1; j < cand.Count; j++)
                    for (int k = j + 1; k < cand.Count; k++)
                    {
                        donors.Clear();
                        donors.Add(cand[i]); donors.Add(cand[i]);
                        donors.Add(cand[j]); donors.Add(cand[j]);
                        donors.Add(cand[k]); donors.Add(cand[k]);
                        if (YieldsTarget(center, donors, target)) return new List<string>(donors);
                    }
            return null;
        }

        // Donor pool: closest-to-target clones (reinforcers) PLUS guaranteed suppliers
        // (>=2 per slot) so filler donors carrying one rare gene aren't excluded.
        private List<string> DonorPool(List<string> bank, string target, int cap)
        {
            var pool = bank.OrderBy(d => WrongSlots(d, target)).Take(cap).ToList();
            for (int s = 0; s < Slots; s++)
            {
                int have = pool.Count(d => d[s] == target[s]);
                if (have < 2)
                    foreach (var d in bank.Where(x => x[s] == target[s] && !pool.Contains(x))
                                          .OrderBy(x => WrongSlots(x, target)).Take(2 - have))
                        pool.Add(d);
            }
            return pool;
        }

        // Probability the donor multiset alone yields 'target' around a THROWAWAY center.
        // Requires the winning gene to dominate (weight > any center = >1.0), i.e. 2+ donors.
        private double GenomePDonorOnly(List<string> donors, string target)
        {
            double p = 1.0;
            for (int s = 0; s < Slots; s++)
            {
                float g = 0, y = 0, h = 0, w = 0, x = 0;
                for (int i = 0; i < donors.Count; i++)
                {
                    char d = donors[i][s];
                    float wt = Wt(d);
                    if (d == 'G') g += wt; else if (d == 'Y') y += wt; else if (d == 'H') h += wt; else if (d == 'W') w += wt; else x += wt;
                }
                float max = g;
                if (y > max) max = y; if (h > max) max = h; if (w > max) max = w; if (x > max) max = x;
                if (max <= cfg.RedWeight + 1e-4f) return 0.0; // must beat any center (red = 1.0)

                int ties = 0;
                if (Approx(g, max)) ties++; if (Approx(y, max)) ties++; if (Approx(h, max)) ties++;
                if (Approx(w, max)) ties++; if (Approx(x, max)) ties++;
                float wantAcc = target[s] == 'G' ? g : target[s] == 'Y' ? y : target[s] == 'H' ? h : target[s] == 'W' ? w : x;
                if (!Approx(wantAcc, max)) return 0.0;
                p *= 1.0 / ties;
            }
            return p;
        }

        // Best throwaway-center recipe: a donor multiset that produces the target
        // around ANY center (the near-perfects are used as donors, not the center).
        private (double p, List<string> donors) BestFinishDonorOnly(string target, List<string> bank, int poolCap, int maxD)
        {
            var pool = DonorPool(bank, target, poolCap);
            if (pool.Count == 0) return (0.0, null);
            double bestP = 0.0; List<string> bestD = null; int evals = 0; const int cap = 150000;
            EnumCombos(pool, 0, Mathf.Clamp(maxD, 2, 8), new List<string>(), donors =>
            {
                if (++evals > cap) return false;
                if (donors.Count < 2) return true; // need 2+ to dominate a center
                double p = GenomePDonorOnly(donors, target);
                if (p > bestP) { bestP = p; bestD = new List<string>(donors); }
                return true;
            });
            return (bestP, bestD);
        }

        // Highest-probability donor multiset that turns 'center' into 'target'.
        // poolCap/maxD bound the search; up to 8 donors = a full planter around the center.
        private (double p, List<string> donors) BestFinish(string center, string target, List<string> bank, int poolCap, int maxD)
        {
            var needed = new List<int>();
            for (int s = 0; s < Slots; s++) if (center[s] != target[s]) needed.Add(s);
            if (needed.Count == 0) return (1.0, new List<string>());

            var pool = DonorPool(bank, target, poolCap);
            if (pool.Count == 0) return (0.0, null);

            double bestP = 0.0; List<string> bestD = null; int evals = 0; const int cap = 120000;
            EnumCombos(pool, 0, Mathf.Clamp(maxD, 2, 8), new List<string>(), donors =>
            {
                if (++evals > cap) return false;
                double p = GenomeP(center, donors, target);
                if (p > bestP) { bestP = p; bestD = new List<string>(donors); }
                return true;
            });
            return (bestP, bestD);
        }

        // Best one-round recipes (any probability), ranked. Tries near-perfect centers
        // AND the throwaway-center arrangement (donors dominate any center).
        private List<DetRecipe> FindBestOneStep(List<string> bank, string target)
        {
            var res = new List<DetRecipe>();
            foreach (var center in bank.OrderBy(g => WrongSlots(g, target)).Take(8))
            {
                var (p, donors) = BestFinish(center, target, bank, 10, 8);
                if (p > 0.01) res.Add(new DetRecipe { Center = center, Donors = donors ?? new List<string>(), P = p });
            }

            var (dp, dd) = BestFinishDonorOnly(target, bank, 14, 8);
            if (dp > 0.01) res.Add(new DetRecipe { AnyCenter = true, Donors = dd, P = dp });

            return res.OrderByDescending(r => r.P).ThenBy(r => r.Donors.Count).Take(cfg.FinderResults).ToList();
        }

        // Clean-donor bridge candidates: genomes 1-2 genes off the target with at most
        // one red, not already in the bank. These are the intermediates you breed first.
        private List<string> GenerateBridges(string target, List<string> bank)
        {
            var bankSet = new HashSet<string>(bank);
            var set = new HashSet<string>();
            var genes = new[] { 'G', 'Y', 'H', 'W', 'X' };
            var arr = target.ToCharArray();

            for (int s = 0; s < Slots; s++)
            {
                char o = arr[s];
                foreach (var ch in genes)
                {
                    if (ch == o) continue;
                    arr[s] = ch; string b = new string(arr);
                    if (b.Count(IsRed) <= 1 && !bankSet.Contains(b)) set.Add(b);
                }
                arr[s] = o;
            }
            for (int s1 = 0; s1 < Slots; s1++)
                for (int s2 = s1 + 1; s2 < Slots; s2++)
                {
                    char o1 = arr[s1], o2 = arr[s2];
                    foreach (var c1 in genes)
                        foreach (var c2 in genes)
                        {
                            if (c1 == o1 || c2 == o2) continue;
                            arr[s1] = c1; arr[s2] = c2; string b = new string(arr);
                            if (b.Count(IsRed) <= 1 && !bankSet.Contains(b)) set.Add(b);
                        }
                    arr[s1] = o1; arr[s2] = o2;
                }

            return set.OrderBy(b => b.Count(IsRed)).ThenBy(b => WrongSlots(b, target)).Take(24).ToList();
        }

        // Best two-round bridge paths: breed a clean bridge donor (round 1), then finish
        // the target with a throwaway-center combine that uses the bridge (round 2).
        private List<TwoStagePath> FindDetPaths(List<string> bank, string target)
        {
            var paths = new List<TwoStagePath>();
            foreach (var bridge in GenerateBridges(target, bank))
            {
                var (p1, d1) = BestFinishDonorOnly(bridge, bank, 10, 6);     // round 1: bank -> bridge
                if (p1 <= 0.1 || d1 == null) continue;

                var bankPlus = new List<string>(bank) { bridge };
                var (p2, d2) = BestFinishDonorOnly(target, bankPlus, 12, 6); // round 2: bank + bridge -> target
                if (p2 <= 0.1 || d2 == null || !d2.Contains(bridge)) continue;

                paths.Add(new TwoStagePath { Bridge = bridge, R1Donors = d1, R2Donors = d2, P = p1 * p2 });
            }
            return paths.OrderByDescending(p => p.P).Take(3).ToList();
        }

        private string CenterLabel(DetRecipe r)
            => r.AnyCenter ? "<i><color=#9fb0a0>any plant</color></i>" : ColorGenesC(r.Center);

        // Short per-option stock check: what to clone up before running this recipe.
        private string StockStatus(List<string> needed, Dictionary<string, int> have)
        {
            var demand = new Dictionary<string, int>();
            foreach (var g in needed) demand[g] = (demand.TryGetValue(g, out var n) ? n : 0) + 1;

            var shorts = new List<string>();
            var lasts = new List<string>();
            foreach (var kv in demand.OrderByDescending(k => k.Value))
            {
                int has = have.TryGetValue(kv.Key, out var h) ? h : 0;
                if (has < kv.Value) shorts.Add($"{kv.Value}x{ColorGenesC(kv.Key)}<color=#9fb0a0>(have {has})</color>");
                else if (has == kv.Value) lasts.Add(ColorGenesC(kv.Key));
            }
            if (shorts.Count > 0) return $"<color=#FF5B5B>clone up:</color> {string.Join(" ", shorts)}";
            if (lasts.Count > 0) return $"<color=#ffd34d>last copies:</color> {string.Join(" ", lasts)} <color=#9fb0a0>— clone a spare</color>";
            return "<color=#7BFF52>you have enough</color>";
        }

        private string FmtDonorList(List<string> donors)
            => donors == null || donors.Count == 0
                ? "<color=#7BFF52>clone it</color>"
                : string.Join(" ", donors.GroupBy(x => x).Select(g => $"{g.Count()}x{ColorGenesC(g.Key)}"));

        // ---- cached route (only recomputes when clones or target change) ----
        private class RouteResult
        {
            public List<DetRecipe> OneStep = new();   // ranked by probability
            public List<TwoStagePath> Paths = new();  // guaranteed two-stage
            public List<string> Advice = new();
            public bool Has100;
        }
        private readonly Dictionary<ulong, (string key, RouteResult res)> routeCache = new();

        private RouteResult GetRoute(BasePlayer player, List<ScannedClone> clones, string typeKey, string target)
        {
            string key = typeKey + "|" + target + "|" +
                         string.Join(",", clones.Select(c => c.Genes).OrderBy(x => x));
            if (routeCache.TryGetValue(player.userID, out var cached) && cached.key == key)
                return cached.res;

            var res = new RouteResult();
            var bank = clones.Select(c => c.Genes).Distinct().ToList();
            if (bank.Count == 0)
            {
                res.Advice = BuildAdvice(clones, target);
            }
            else
            {
                res.OneStep = FindBestOneStep(bank, target);
                res.Has100 = res.OneStep.Any(r => r.P >= 0.999);
                if (!res.Has100)
                {
                    res.Paths = FindDetPaths(bank, target); // guaranteed two-stage, if any
                    if (res.Paths.Count == 0 && res.OneStep.Count == 0)
                        res.Advice = BuildAdvice(clones, target);
                }
            }

            routeCache[player.userID] = (key, res);
            return res;
        }

        #endregion

        #region Data (per-player targets)

        private class BankData
        {
            public Dictionary<ulong, string> Targets = new();
        }

        private BankData data;
        private const string DataFile = "RustGeneticsHelper_Banks";

        private void LoadData()
        {
            try { data = Interface.Oxide.DataFileSystem.ReadObject<BankData>(DataFile) ?? new BankData(); }
            catch { data = new BankData(); }
            data.Targets ??= new Dictionary<ulong, string>();
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);

        private string GetTarget(BasePlayer p)
            => data.Targets.TryGetValue(p.userID, out var t) && NormalizeGenome(t) != null ? t : NormalizeTarget(cfg.TargetGenome);

        private void SetTarget(BasePlayer p, string genome)
        {
            data.Targets[p.userID] = genome;
            SaveData();
        }

        #endregion

        #region Shared UI helpers

        private string ColorGenes(string genome)
        {
            var sb = new StringBuilder();
            foreach (var ch in genome)
            {
                string col = IsRed(ch) ? cfg.RedColorHex : cfg.GreenColorHex;
                sb.Append($"<color={col}><b>{ch}</b></color> ");
            }
            return sb.ToString().TrimEnd();
        }

        // Compact (no spaces between letters) — used in dense recipe lines.
        private string ColorGenesC(string genome)
        {
            var sb = new StringBuilder();
            foreach (var ch in genome)
                sb.Append($"<color={(IsRed(ch) ? cfg.RedColorHex : cfg.GreenColorHex)}><b>{ch}</b></color>");
            return sb.ToString();
        }

        private static string Pct(double p) => $"{Math.Round(p * 100)}%";

        #endregion

        #region Clone scanning + main panel

        private const string MainPanel = "geneticshelper.main";
        private const string MainContent = "geneticshelper.main.content";
        private readonly HashSet<ulong> mainPanelOpen = new();
        private readonly Dictionary<ulong, int> clonePage = new();
        private readonly Dictionary<ulong, int> typeIndex = new();
        private readonly HashSet<ulong> flushPending = new();

        private static string F(float v) => v.ToString("0.000", System.Globalization.CultureInfo.InvariantCulture);

        // ---- item gene decoding ----
        // Rust stores clone/seed genes in item.instanceData.dataInt, decoded by
        // GrowableGeneEncoding.DecodeIntToGeneString(int) -> "GYHWXG".
        private static bool _decodeResolved;
        private static MethodInfo _decodeStr;    // int -> string   (preferred)
        private static MethodInfo _decodeFill;   // (int, GrowableGenes) fill
        private static Type _genesType;

        private static void ResolveGeneDecoder()
        {
            _decodeResolved = true;
            try
            {
                var encType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(SafeGetTypes)
                    .FirstOrDefault(t => t.Name == "GrowableGeneEncoding");
                if (encType == null) return;

                foreach (var m in encType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
                {
                    if (m.Name.IndexOf("Previous", StringComparison.OrdinalIgnoreCase) >= 0) continue; // pre-crossbreed genes
                    var ps = m.GetParameters();

                    if (_decodeStr == null && ps.Length == 1 && ps[0].ParameterType == typeof(int)
                        && m.ReturnType == typeof(string) && m.Name.IndexOf("Decode", StringComparison.OrdinalIgnoreCase) >= 0)
                        _decodeStr = m;

                    if (_decodeFill == null && ps.Length == 2 && ps[0].ParameterType == typeof(int)
                        && m.ReturnType == typeof(void) && ps[1].ParameterType.Name.IndexOf("Gene", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        _decodeFill = m;
                        _genesType = ps[1].ParameterType;
                    }
                }
            }
            catch { }
        }

        private static IEnumerable<Type> SafeGetTypes(Assembly a)
        {
            try { return a.GetTypes(); } catch { return Array.Empty<Type>(); }
        }

        private string ReadItemGenome(Item item)
        {
            try
            {
                if (item?.instanceData == null) return null;
                int enc = item.instanceData.dataInt;
                if (enc == 0) return null;
                if (!_decodeResolved) ResolveGeneDecoder();

                string raw = null;
                if (_decodeStr != null)
                {
                    raw = _decodeStr.Invoke(null, new object[] { enc }) as string;
                }
                else if (_decodeFill != null && _genesType != null)
                {
                    object genes = Activator.CreateInstance(_genesType, true);
                    _decodeFill.Invoke(null, new object[] { enc, genes });
                    raw = ReadGenomeFromGenetics(genes);
                }
                return CleanGenome(raw);
            }
            catch { return null; }
        }

        // Uppercase, keep only gene letters, require exactly 6.
        private static string CleanGenome(string s)
        {
            if (string.IsNullOrEmpty(s)) return null;
            var sb = new StringBuilder(Slots);
            foreach (var ch in s.ToUpper())
                if (IsValidGene(ch)) sb.Append(ch);
            return sb.Length == Slots ? sb.ToString() : null;
        }

        // Count (destroy=false) or destroy (destroy=true) all clones with >3 reds
        // in the player's own inventory and their own nearby boxes. Returns the copy count.
        private int FlushJunk(BasePlayer player, bool destroy)
        {
            int count = 0;
            var junk = new List<Item>();

            foreach (var it in AllInventoryItems(player))
            {
                string g = ReadItemGenome(it);
                if (g != null && g.Count(IsRed) > 3) { count += Math.Max(1, it.amount); junk.Add(it); }
            }
            foreach (var box in GetNearbyOwnedContainers(player, cfg.BoxScanRadius))
            {
                if (box == null || box.OwnerID != player.userID || box.inventory?.itemList == null) continue;
                foreach (var it in box.inventory.itemList)
                {
                    string g = ReadItemGenome(it);
                    if (g != null && g.Count(IsRed) > 3) { count += Math.Max(1, it.amount); junk.Add(it); }
                }
            }

            if (destroy) foreach (var it in junk) it.Remove();
            return count;
        }

        private static IEnumerable<Item> AllInventoryItems(BasePlayer player)
        {
            var list = new List<Item>();
            var inv = player?.inventory;
            if (inv == null) return list;
            if (inv.containerMain?.itemList != null) list.AddRange(inv.containerMain.itemList);
            if (inv.containerBelt?.itemList != null) list.AddRange(inv.containerBelt.itemList);
            if (inv.containerWear?.itemList != null) list.AddRange(inv.containerWear.itemList);
            return list;
        }

        // Move the clones a recipe needs from nearby owned boxes into the player's
        // main inventory, up to the counts required and while there is room.
        private void PullClones(BasePlayer player, List<string> needed)
        {
            if (needed == null || needed.Count == 0) { player.ChatMessage("Nothing to pull for that round."); return; }

            var need = new Dictionary<string, int>();
            foreach (var g in needed) need[g] = (need.TryGetValue(g, out var n) ? n : 0) + 1;

            // Subtract what's already on the player.
            foreach (var it in AllInventoryItems(player))
            {
                string g = ReadItemGenome(it);
                if (g != null && need.TryGetValue(g, out var want) && want > 0)
                    need[g] = Math.Max(0, want - Math.Max(1, it.amount));
            }

            // Already have everything the recipe needs?
            if (need.Values.All(v => v <= 0))
            {
                player.ChatMessage("You already have the needed clones in your inventory.");
                return;
            }

            var main = player.inventory.containerMain;
            int pulled = 0; bool full = false;
            foreach (var box in GetNearbyOwnedContainers(player, cfg.BoxScanRadius))
            {
                if (box?.inventory?.itemList == null) continue;
                foreach (var item in box.inventory.itemList.ToList())
                {
                    string g = ReadItemGenome(item);
                    if (g == null || !need.TryGetValue(g, out var want) || want <= 0) continue;

                    int take = Math.Min(want, Math.Max(1, item.amount));
                    Item moving = take >= item.amount ? item : item.SplitItem(take);
                    if (moving == null) continue;
                    if (!moving.MoveToContainer(main))
                    {
                        if (moving != item) moving.MoveToContainer(box.inventory); // put split back
                        full = true; break;
                    }
                    need[g] = want - take; pulled += take;
                }
                if (full) break;
            }

            int missing = need.Values.Sum(v => Math.Max(0, v));
            string msg;
            if (pulled > 0)
            {
                msg = $"Pulled {pulled} clone(s) into your inventory.";
                if (full) msg += " Inventory is full — free space and pull again.";
                else if (missing > 0) msg += $" {missing} more aren't in nearby boxes — grow or breed them.";
            }
            else
            {
                msg = full
                    ? "Inventory is full — free space and pull again."
                    : "The remaining clones aren't in nearby boxes — grow or breed them.";
            }
            player.ChatMessage(msg);
        }

        // ---- scanning inventory + owned nearby boxes ----
        private class ScannedClone { public string Genes; public int Count; public int InvCount; public int BoxCount; public bool Inv; public bool Box; public string TypeKey; public string TypeName; }

        private List<ScannedClone> ScanClones(BasePlayer player, out int boxCount)
        {
            var map = new Dictionary<string, ScannedClone>();

            void Add(Item it, bool inv)
            {
                if (it == null) return;
                string g = ReadItemGenome(it);
                if (g == null) return;
                string typeKey = it.info?.shortname ?? "clone";
                string key = typeKey + "\u0001" + g;
                if (!map.TryGetValue(key, out var sc))
                    map[key] = sc = new ScannedClone { Genes = g, TypeKey = typeKey, TypeName = PrettyTypeName(typeKey) };
                int amt = Math.Max(1, it.amount);
                sc.Count += amt;
                if (inv) { sc.Inv = true; sc.InvCount += amt; }
                else { sc.Box = true; sc.BoxCount += amt; }
            }

            foreach (var it in AllInventoryItems(player)) Add(it, true);

            var boxes = GetNearbyOwnedContainers(player, cfg.BoxScanRadius);
            boxCount = boxes.Count;
            foreach (var box in boxes)
                if (box?.inventory?.itemList != null)
                    foreach (var it in box.inventory.itemList) Add(it, false);

            return map.Values
                .OrderBy(s => s.Genes.Count(IsRed))
                .ThenByDescending(s => s.Count)
                .ToList();
        }

        // "clone.blue.berry" -> "BLUE BERRY", "clone.hemp" -> "HEMP"
        private static string PrettyTypeName(string shortname)
        {
            string s = shortname ?? "clone";
            if (s.StartsWith("clone.")) s = s.Substring(6);
            else if (s.StartsWith("seed.")) s = s.Substring(5);
            return s.Replace(".", " ").Replace("_", " ").ToUpper();
        }

        private List<StorageContainer> GetNearbyOwnedContainers(BasePlayer player, float radius)
        {
            var result = new List<StorageContainer>();
            try
            {
                int count = BaseEntity.Query.Server.GetInSphere(player.transform.position, radius, _queryBuffer,
                    e => e is StorageContainer);
                for (int i = 0; i < count && i < _queryBuffer.Length; i++)
                {
                    if (_queryBuffer[i] is StorageContainer sc && !sc.IsDestroyed && sc.inventory != null)
                    {
                        if (sc.OwnerID == player.userID || (cfg.ScanTeammateBoxes && IsTeammate(player, sc.OwnerID)))
                            result.Add(sc);
                    }
                }
            }
            catch { }
            return result;
        }

        private static bool IsTeammate(BasePlayer player, ulong other)
        {
            if (other == 0 || player.currentTeam == 0) return false;
            var team = RelationshipManager.ServerInstance?.FindTeam(player.currentTeam);
            return team?.members != null && team.members.Contains(other);
        }

        // ---- advice when no recipe is reachable ----
        private List<string> BuildAdvice(List<ScannedClone> clones, string target)
        {
            var advice = new List<string>();
            for (int s = 0; s < Slots; s++)
            {
                char want = target[s];
                if (IsRed(want)) continue;
                int sources = clones.Count(c => c.Genes[s] == want);
                if (sources == 0)
                    advice.Add($"Slot {s + 1}: no clone has <b>{ColorGenes(want.ToString())}</b> — grow seeds to find one.");
                else if (sources == 1)
                    advice.Add($"Slot {s + 1}: only 1 source of <b>{ColorGenes(want.ToString())}</b> — get a 2nd to beat reds.");
            }
            if (advice.Count == 0)
                advice.Add("<color=#9fb0a0>You have every gene, but they're spread across reddy clones. Grow cleaner clones (fewer W/X) so a bridge becomes possible.</color>");
            return advice;
        }

        // ---- the panel ----
        // Split into a static SHELL (built once; owns the cursor) and a DYNAMIC
        // content layer that is refreshed in place. Refreshing only the content
        // means the cursor never recenters and there's no full-panel flash.
        private void OpenMainPanel(BasePlayer player)
        {
            mainPanelOpen.Add(player.userID);
            clonePage[player.userID] = 0;
            BuildShell(player);
            RefreshContent(player);
        }

        private void CloseMainPanel(BasePlayer player)
        {
            mainPanelOpen.Remove(player.userID);
            CuiHelper.DestroyUi(player, MainContent);
            CuiHelper.DestroyUi(player, MainPanel);
        }

        private void BuildShell(BasePlayer player)
        {
            CuiHelper.DestroyUi(player, MainPanel);
            var c = new CuiElementContainer();

            string root = c.Add(new CuiPanel
            {
                Image = { Color = "0.06 0.07 0.06 0.98" },
                RectTransform = { AnchorMin = "0.30 0.15", AnchorMax = "0.70 0.87" },
                CursorEnabled = true
            }, "Overlay", MainPanel);

            // title bar (static parts)
           
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху