using System; using System.Collections.Generic; using Oxide.Core.Plugins; using UnityEngine; /* * MajestatTopLoot 1.0.1 * ====================== * Moduł dla MajestatTopCore 3.0.0 — zlicza otwarte skrzynki, rozbite beczki i złom. * * WYMAGA: MajestatTopCore 3.0.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.1")] [Description("Loot module for MajestatTopCore — tracks crates, barrels and scrap.")] class MajestatTopLoot : RustPlugin { [PluginReference] Plugin MajestatTopCore; private bool _registered = false; // 0 = tylko przy starcie, np. 720 = co 12h private int _updateIntervalMinutes = 0; // ── Update checker ──────────────────────────────────────────── private const string PluginVersion = "1.0.1"; private const string PluginUpdateUrl = "http://download.koldrix.com/rust/plugins/MajestatTopLoot/version.json"; // 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("MajestatTopCore nie jest załadowany! Moduł nieaktywny."); return; } RegisterTab(); } void OnPluginLoaded(Plugin plugin) { if (plugin?.Name == "MajestatTopCore" && !_registered) 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" }, new Dictionary { ["key"] = "Loot.EliteCrate", ["header"] = "ELITE" }, new Dictionary { ["key"] = "Loot.MilitaryCrate", ["header"] = "MILITARY" }, new Dictionary { ["key"] = "Loot.LockedCrate", ["header"] = "LOCKED" }, new Dictionary { ["key"] = "Loot.HackableCrate", ["header"] = "CH47" }, new Dictionary { ["key"] = "Loot.Airdrop", ["header"] = "AIRDROP" }, new Dictionary { ["key"] = "Loot.UnderwaterCrate",["header"] = "PODWODNE" }, new Dictionary { ["key"] = "Loot.NormalCrate", ["header"] = "SKRZYNKI" }, new Dictionary { ["key"] = "Loot.Barrel", ["header"] = "BECZKI" }, }; var tabDef = new Dictionary { ["id"] = "loot", ["label"] = "LOOT", ["sort"] = "Loot.Total", ["fame_sort"] = "Loot.Total", ["order"] = 15, ["columns"] = columns }; bool ok = (bool)(MajestatTopCore.Call("API_RegisterTab", tabDef) ?? false); if (!ok) { PrintError("Rejestracja zakładki LOOT nie powiodła się."); 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; // Pobierz interwał z konfiguracji MajestatTopCore var intervalObj = MajestatTopCore.Call("API_GetUpdateInterval"); _updateIntervalMinutes = intervalObj is int iv ? iv : 0; timer.Once(5f, CheckForUpdate); if (_updateIntervalMinutes > 0) timer.Every(_updateIntervalMinutes * 60f, CheckForUpdate); Puts("Zakładka LOOT zarejestrowana. Komendy: /toploot, /fameloot"); } // ═════════════════════════════════════════════════════════════ // 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); Puts($"CRATE: {player.displayName} otworzył [{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); Puts($"BARREL: {attacker.displayName} rozbił [{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) { // Typowe prefaby beczek w Rust: // loot_barrel_1, loot_barrel_2, loot-barrel-1, loot-barrel-2 // oil_barrel, diesel_barrel_world // trash-pile-1 (śmietniki) 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 — niezależny od MajestatTopCore // ═════════════════════════════════════════════════════════════ private void CheckForUpdate() { Puts($"[Update] Sprawdzam aktualizacje... (aktualna wersja: {PluginVersion})"); webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void OnUpdateResponse(int code, string response) { if (code == 0 || code >= 400 || string.IsNullOrEmpty(response)) { Puts($"[Update] Nie można sprawdzić aktualizacji — serwer niedostępny (HTTP {code})."); return; } try { var data = JsonConvert.DeserializeObject>(response); if (data == null || !data.ContainsKey("version")) { Puts("[Update] Nieprawidłowy plik version.json."); return; } string latest = data["version"].Trim(); if (IsNewerVersion(latest, PluginVersion)) { string url = data.ContainsKey("url") ? data["url"] : PluginUpdateUrl; PrintWarning($"[Update] Dostępna nowa wersja: {latest} (zainstalowana: {PluginVersion})"); PrintWarning($"[Update] Pobierz: {url}"); } else { Puts($"[Update] Wersja aktualna ({PluginVersion}) — brak aktualizacji."); } } catch (Exception ex) { Puts($"[Update] Błąd odczytu odpowiedzi: {ex.Message}"); } } private bool IsNewerVersion(string latest, string current) { try { var l = System.Array.ConvertAll(latest.Split('.'), int.Parse); var c = System.Array.ConvertAll(current.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; } } } }