using Newtonsoft.Json; using System; using System.Collections.Generic; using Oxide.Core.Plugins; using UnityEngine; /* * MajestatTopLoot 1.0.1 * ====================== * Moduł dla MajestatTopCore 3.2.0 — zlicza otwarte skrzynki, rozbite beczki i złom. * * WYMAGA: MajestatTopCore 3.2.0+ * * JAK TO DZIAŁA: * Skrzynki (otwieranie E): OnLootEntity * Beczki (rozbijanie): OnEntityDeath — beczka ginie → przypisz ostatniemu atakującemu * Złom ze skrzynki: OnItemPickup gdy _looters zawiera gracza * Złom z rozbitej beczki: OnEntityDeath → zapisujemy pozycję beczki → * OnItemPickup w pobliżu tej pozycji * Złom z ziemi (collectible): OnCollectiblePickup * Złom z węzłów: OnDispenserGather * * Śledzone statystyki: * Loot.Total — skrzynki + beczki otwarte/rozbite łącznie * Loot.EliteCrate — elite crate * Loot.MilitaryCrate — military crate, tunnel crate * Loot.NormalCrate — zwykłe skrzynki (crate_normal, crate_tools itd.) * Loot.LockedCrate — locked crate (Heli/Bradley) * Loot.UnderwaterCrate— skrzynki podwodne * Loot.Airdrop — supply drop * Loot.HackableCrate — hackable crate (CH47/Oil Rig) * Loot.Barrel — rozbite beczki (osobna kolumna od skrzynek) * TOTAL nie zawiera złomu — tylko skrzynki i beczki. */ namespace Oxide.Plugins { [Info("MajestatTopLoot", "Wo0t", "1.0.2")] [Description("Loot module for MajestatTopCore — tracks crates, barrels and scrap.")] class MajestatTopLoot : RustPlugin { [PluginReference] Plugin MajestatTopCore; private bool _registered = false; // Poziom logowania pobierany z Core (0=all 1=system 2=alert 3=none) private int _logLevel = 1; private void MLog(int level, string msg) { int effective = MajestatTopCore != null ? (int)(MajestatTopCore.Call("API_GetLogLevel") ?? 1) : _logLevel; if (level < effective) return; // Podczas AutoReload wycisz komunikaty systemowe (1) — Core drukuje summary if (level == 1 && MajestatTopCore != null) { var ar = MajestatTopCore.Call("API_IsAutoReloading"); if (ar is bool b && b) return; } if (level >= 2) PrintWarning(msg); else Puts(msg); } // ── Update checker ──────────────────────────────────────────── private const string PluginVersion = "1.0.2"; private static readonly string PluginUpdateUrl = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String("aHR0cDovL2Rvd25sb2FkLmtvbGRyaXguY29tL3J1c3QvcGx1Z2lucy9NYWplc3RhdFRvcExvb3QvdmVyc2lvbi5qc29u")); // Gracze aktualnie mający otwartą skrzynkę (do śledzenia złomu) private Dictionary _looters = new Dictionary(); // Pary playerID+entityID które już zostały policzone (zapobiega wielokrotnemu liczeniu) // Gracz musi zamknąć i otworzyć INNĄ skrzynkę żeby zaliczyło kolejny punkt private HashSet _counted = new HashSet(); // Pozycje rozbitych beczek + kto je rozbił (do złomu z beczki) // Klucz: Vector3 pozycja (zaokrąglona), wartość: steamId private Dictionary _barrelScrap = new Dictionary(); // ═════════════════════════════════════════════════════════════ // INIT // ═════════════════════════════════════════════════════════════ void OnServerInitialized() { if (MajestatTopCore == null) { PrintWarning("[MajestatTopLoot] MajestatTopCore is not loaded! Module inactive."); return; } if (!CheckMinVersion(MajestatTopCore.Version, 3, 2, 0)) { PrintError($"[MajestatTopLoot] Requires MajestatTopCore 3.2.0+. Installed: {MajestatTopCore.Version}. Module inactive."); return; } RegisterTab(); } private static bool CheckMinVersion(Oxide.Core.VersionNumber v, int major, int minor, int patch) => v.Major > major || (v.Major == major && (v.Minor > minor || (v.Minor == minor && v.Patch >= patch))); void OnPluginLoaded(Plugin plugin) { if (plugin?.Name == "MajestatTopCore" && !_registered) { if (!CheckMinVersion(plugin.Version, 3, 2, 0)) { PrintError($"[MajestatTopLoot] Requires MajestatTopCore 3.2.0+. Installed: {plugin.Version}."); return; } RegisterTab(); } } void OnPluginUnloaded(Plugin plugin) { if (plugin?.Name == "MajestatTopCore") _registered = false; } void Unload() { if (MajestatTopCore != null && _registered) MajestatTopCore.Call("API_UnregisterTab", "loot"); _looters.Clear(); _barrelScrap.Clear(); _counted.Clear(); } // ═════════════════════════════════════════════════════════════ // REJESTRACJA ZAKŁADKI // ═════════════════════════════════════════════════════════════ private void RegisterTab() { if (MajestatTopCore == null) return; var columns = new List> { new Dictionary { ["key"] = "Loot.Total", ["header"] = "TOTAL", ["lang_key"] = "Loot.Total" }, new Dictionary { ["key"] = "Loot.EliteCrate", ["header"] = "ELITE", ["lang_key"] = "Loot.EliteCrate" }, new Dictionary { ["key"] = "Loot.MilitaryCrate", ["header"] = "MILITARY", ["lang_key"] = "Loot.MilitaryCrate" }, new Dictionary { ["key"] = "Loot.LockedCrate", ["header"] = "LOCKED", ["lang_key"] = "Loot.LockedCrate" }, new Dictionary { ["key"] = "Loot.HackableCrate", ["header"] = "CH47", ["lang_key"] = "Loot.HackableCrate" }, new Dictionary { ["key"] = "Loot.Airdrop", ["header"] = "AIRDROP", ["lang_key"] = "Loot.Airdrop" }, new Dictionary { ["key"] = "Loot.UnderwaterCrate",["header"] = "PODWODNE", ["lang_key"] = "Loot.UnderwaterCrate" }, new Dictionary { ["key"] = "Loot.NormalCrate", ["header"] = "SKRZYNKI", ["lang_key"] = "Loot.NormalCrate" }, new Dictionary { ["key"] = "Loot.Barrel", ["header"] = "BECZKI", ["lang_key"] = "Loot.Barrel" }, }; var tabDef = new Dictionary { ["id"] = "loot", ["label"] = "LOOT", ["sort"] = "Loot.Total", ["fame_sort"] = "Loot.Total", ["order"] = 15, ["columns"] = columns }; lang.RegisterMessages(new Dictionary { ["Loot.Total"] = "TOTAL", ["Loot.EliteCrate"] = "ELITE CRATE", ["Loot.MilitaryCrate"] = "MILITARY", ["Loot.LockedCrate"] = "LOCKED", ["Loot.HackableCrate"] = "CH47", ["Loot.Airdrop"] = "AIRDROP", ["Loot.UnderwaterCrate"]= "UNDERWATER", ["Loot.NormalCrate"] = "CRATES", ["Loot.Barrel"] = "BARREL", }, this); lang.RegisterMessages(new Dictionary { ["Loot.Total"] = "TOTAL", ["Loot.EliteCrate"] = "ELITE", ["Loot.MilitaryCrate"] = "MILITARY", ["Loot.LockedCrate"] = "LOCKED", ["Loot.HackableCrate"] = "CH47", ["Loot.Airdrop"] = "AIRDROP", ["Loot.UnderwaterCrate"]= "PODWODNE", ["Loot.NormalCrate"] = "SKRZYNKI", ["Loot.Barrel"] = "BECZKI", }, this, "pl"); bool ok = (bool)(MajestatTopCore.Call("API_RegisterTab", tabDef) ?? false); if (!ok) { PrintError("LOOT tab registration failed."); return; } MajestatTopCore.Call("API_RegisterXpSource", "Loot.EliteCrate", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.MilitaryCrate", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.LockedCrate", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.HackableCrate", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.Airdrop", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.UnderwaterCrate", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.NormalCrate", 0.0); MajestatTopCore.Call("API_RegisterXpSource", "Loot.Barrel", 0.0); _registered = true; MajestatTopCore.Call("API_RegisterModule", new Dictionary { ["name"] = "MajestatTopLoot", ["version"] = PluginVersion, ["author"] = "Wo0t", ["description"] = "Loot stats module" }); // startup log handled by Core } // ═════════════════════════════════════════════════════════════ // HOOK — GRACZ OTWIERA SKRZYNKĘ (klawisz E) // Beczki tu NIE trafiają — są rozbijane, nie otwierane // ═════════════════════════════════════════════════════════════ void OnLootEntity(BasePlayer player, BaseEntity entity) { if (player == null || entity == null || MajestatTopCore == null) return; string prefab = entity.ShortPrefabName?.ToLower() ?? ""; string statKey = ResolveCrateKey(prefab); if (statKey == null) return; // Zapamiętaj że gracz ma otwartą skrzynkę (do śledzenia złomu) _looters[player.userID] = player.UserIDString; // Sprawdź czy ta konkretna skrzynka przez tego gracza była już policzona // Gracz może wielokrotnie klikać E — liczymy tylko pierwsze otwarcie string countKey = $"{player.userID}_{entity.net?.ID.Value}"; if (_counted.Contains(countKey)) return; _counted.Add(countKey); MajestatTopCore.Call("API_AddStat", player.UserIDString, statKey, 1.0, true, true); MajestatTopCore.Call("API_AddStat", player.UserIDString, "Loot.Total", 1.0, true, false); MLog(0, $"CRATE: {player.displayName} opened [{prefab}] -> {statKey}"); } void OnLootEntityEnd(BasePlayer player, BaseCombatEntity entity) { if (player == null) return; _looters.Remove(player.userID); } // ═════════════════════════════════════════════════════════════ // HOOK — BECZKA ROZBITA (OnEntityDeath) // Beczki są niszczone, nie otwierane — używamy OnEntityDeath // ═════════════════════════════════════════════════════════════ void OnEntityDeath(BaseCombatEntity entity, HitInfo info) { if (entity == null || MajestatTopCore == null) return; string prefab = entity.ShortPrefabName?.ToLower() ?? ""; if (!IsBarrel(prefab)) return; // Znajdź kto rozbił beczkę var attacker = info?.InitiatorPlayer; if (attacker == null || attacker.IsNpc) attacker = entity.lastAttacker as BasePlayer; if (attacker == null || attacker.IsNpc) return; MajestatTopCore.Call("API_AddStat", attacker.UserIDString, "Loot.Barrel", 1.0, true, true); MajestatTopCore.Call("API_AddStat", attacker.UserIDString, "Loot.Total", 1.0, true, false); MLog(0, $"BARREL: {attacker.displayName} broke [{prefab}]"); // Zapamiętaj pozycję beczki na 10 sekund // Złom który gracz podniesie z tej pozycji zostanie mu przypisany string posKey = PosKey(entity.transform.position); _barrelScrap[posKey] = attacker.UserIDString; timer.Once(10f, () => _barrelScrap.Remove(posKey)); } // ═════════════════════════════════════════════════════════════ // HELPERY // ═════════════════════════════════════════════════════════════ // Klucz pozycji zaokrąglony do 2m (żeby podnoszenie złomu w pobliżu działało) private string PosKey(Vector3 pos) => $"{Mathf.RoundToInt(pos.x / 2)},{Mathf.RoundToInt(pos.y / 2)},{Mathf.RoundToInt(pos.z / 2)}"; // Znajdź steamId właściciela beczki w promieniu ~3m od gracza private string FindNearbyBarrelOwner(Vector3 playerPos) { string playerKey = PosKey(playerPos); // Sprawdź klucz gracza i sąsiednie komórki gridu (promień ~2-4m) for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) { var p = playerPos + new Vector3(dx * 2, 0, dz * 2); string key = PosKey(p); if (_barrelScrap.TryGetValue(key, out string steamId)) return steamId; } return null; } // ── Czy to beczka? ─────────────────────────────────────────── private bool IsBarrel(string prefab) { return prefab.Contains("loot_barrel") || prefab.Contains("loot-barrel") || prefab.Contains("oil_barrel") || prefab.Contains("diesel_barrel") || prefab.Contains("trash-pile") || prefab.Contains("trash_pile") || prefab.Contains("foodbox"); } // ── Mapowanie prefab → kategoria skrzynki ─────────────────── // UWAGA: kolejność ma znaczenie — bardziej specyficzne warunki pierwsze private string ResolveCrateKey(string prefab) { if (prefab == null) return null; // Beczki — obsługiwane przez OnEntityDeath, nie tu if (IsBarrel(prefab)) return null; // Locked crate — z Bradleya i Helikoptera (SPECYFICZNE — sprawdź pierwsze) if (prefab == "heli_crate" || prefab == "bradley_crate" || prefab.Contains("locked_crate")) return "Loot.LockedCrate"; // Hackable crate — CH47 i Oil Rig (SPECYFICZNE) if (prefab == "codelockedhackablecrate" || prefab == "codelockedhackablecrate_oilrig" || prefab.Contains("hackablelockedcrate")) return "Loot.HackableCrate"; // Elite crate if (prefab == "crate_elite" || prefab.Contains("crate_elite")) return "Loot.EliteCrate"; // Military crate if (prefab == "crate_military_1" || prefab == "crate_military_2" || prefab.Contains("crate_military") || prefab.Contains("military_tunnel_crate") || prefab.Contains("crate_mine")) return "Loot.MilitaryCrate"; // Airdrop — supply drop if (prefab == "supply_drop" || prefab.Contains("supply_drop") || prefab.Contains("supplydrop")) return "Loot.Airdrop"; // Underwater crate if (prefab.Contains("crate_underwater") || prefab.Contains("underwater_lab_yellow") || prefab.Contains("underwater_lab_blue")) return "Loot.UnderwaterCrate"; // Normalne skrzynki (NIE beczki) if (prefab == "crate_normal" || prefab == "crate_normal_2" || prefab == "crate_tools" || prefab == "crate_food_1" || prefab == "crate_food_2" || prefab == "crate_medical" || prefab == "crate_basic" || prefab == "tech_parts_1" || prefab == "tech_parts_2") return "Loot.NormalCrate"; return null; } // ═════════════════════════════════════════════════════════════ // UPDATE CHECKER — synchronizowany przez MajestatTopCore // Core wywołuje hook OnMajestatUpdateCheck na wszystkich modułach // ═════════════════════════════════════════════════════════════ void OnMajestatUpdateCheck() { webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void OnUpdateResponse(int code, string response) { string pluginName = "MajestatTopLoot"; if (code == 0 || code >= 400 || string.IsNullOrEmpty(response)) { MLog(2, $"[Update] Update check failed - server unavailable (HTTP {code})."); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); return; } try { var data = Newtonsoft.Json.JsonConvert.DeserializeObject>(response); string latest = data != null && data.ContainsKey("version") ? data["version"].Trim() : null; if (latest != null && IsNewerVersion(latest, PluginVersion)) { string url = data.ContainsKey("url") ? data["url"] : PluginUpdateUrl; PrintWarning($"[Update] Newer version available ({latest}) - your version ({PluginVersion})"); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, latest, url); } else { MLog(1, $"[Update] Up to date ({PluginVersion}) - no updates."); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); } } catch (Exception ex) { MLog(2, $"[Update] Error: {ex.Message}"); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); } } private bool IsNewerVersion(string latest, string current) { try { string lc = latest != null && latest.Contains("-") ? latest.Substring(0, latest.IndexOf('-')) : latest; string cc = current != null && current.Contains("-") ? current.Substring(0, current.IndexOf('-')) : current; var l = System.Array.ConvertAll(lc.Split('.'), int.Parse); var c = System.Array.ConvertAll(cc.Split('.'), int.Parse); int len = System.Math.Max(l.Length, c.Length); for (int i = 0; i < len; i++) { int lv = i < l.Length ? l[i] : 0; int cv = i < c.Length ? c[i] : 0; if (lv > cv) return true; if (lv < cv) return false; } return false; } catch { return false; } } } }