C#:
// Forced Recompile: 2026-02-12 01:38
using System;
using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Plugins;
using UnityEngine;
using System.Linq;
namespace Oxide.Plugins
{
[Info("NWGPiracy", "NWG Team", "1.1.0")]
[Description("High-seas piracy features: Tugboat Raiders and Deep Sea Salvage with map markers.")]
public class NWGPiracy : RustPlugin
{
private List<BaseEntity> _activePirateEntities = new List<BaseEntity>();
private List<MapMarkerGenericRadius> _activeMarkers = new List<MapMarkerGenericRadius>();
private Timer _piracyTimer;
#region Lifecycle
private void OnServerInitialized()
{
_piracyTimer = timer.Every(3600, SpawnPiracyEvent); // Every hour
}
private void Unload()
{
CleanupPirates();
CleanupMarkers();
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["TugboatSpawned"] = "<color=#b7d092>[NWG] PIRATE TUGBOAT SPOTTED!</color>\nA hostile tugboat with high-value cargo has been spotted at sea!\n<color=#aaaaaa>Check your map for a blue marker.</color>",
["SalvageSpawned"] = "<color=#FFA500>[NWG] DEEP SEA SALVAGE DETECTED!</color>\nA sunken crate with valuable cargo has been located!\n<color=#aaaaaa>Check your map for a cyan marker. Bring diving gear!</color>",
["TugboatAdmin"] = "<color=#b7d092>[NWG]</color> Pirate Tugboat spawned!",
["SalvageAdmin"] = "<color=#b7d092>[NWG]</color> Deep Sea Salvage spawned!",
["LogTugboat"] = "[NWG] A Pirate Tugboat with high-value cargo has been spotted at {0}!",
["LogSalvage"] = "[NWG] Deep sea salvage detected at {0}!"
}, this);
}
private string GetMessage(string key, string userId, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);
#endregion
#region Map Markers
private void CleanupPirates()
{
// Kill in reverse order so children are destroyed before their parents,
// avoiding cascading OnParentRemoved crashes on BasePlayer/HumanNPC entities.
// Also explicitly check for children.
var list = _activePirateEntities.ToList();
list.Reverse();
foreach (var ent in list)
{
try
{
if (ent == null || ent.IsDestroyed) continue;
// Specific check for Tugboat to kill potential leftover mounts/children first if any
if (ent is Tugboat tug)
{
foreach(var child in tug.children.ToList())
{
if (child != null && !child.IsDestroyed) child.Kill();
}
}
ent.Kill();
}
catch (Exception ex)
{
PrintWarning($"[NWG Piracy] Error cleaning up entity: {ex.Message}");
}
}
_activePirateEntities.Clear();
}
private void CleanupMarkers()
{
foreach (var marker in _activeMarkers)
{
if (marker != null && !marker.IsDestroyed) marker.Kill();
}
_activeMarkers.Clear();
}
private void SpawnMapMarker(Vector3 pos, Color primary, Color secondary, float radius, string label)
{
var marker = GameManager.server.CreateEntity("assets/prefabs/tools/map/genericradiusmarker.prefab", pos) as MapMarkerGenericRadius;
if (marker == null) return;
marker.alpha = 0.75f;
marker.color1 = primary;
marker.color2 = secondary;
marker.radius = radius;
marker.Spawn();
marker.SendUpdate();
_activeMarkers.Add(marker);
_activePirateEntities.Add(marker); // Also track for global cleanup
}
#endregion
#region Events
private void SpawnPiracyEvent()
{
int rand = UnityEngine.Random.Range(0, 2);
if (rand == 0) SpawnTugboatRaider();
else SpawnDeepSeaSalvage();
}
[ChatCommand("spawnpiracy")]
private void CmdPiracySpawn(BasePlayer player, string cmd, string[] args)
{
if (!player.IsAdmin) return;
if (args.Length > 0 && args[0].ToLower() == "salvage")
{
SpawnDeepSeaSalvage();
player.ChatMessage(GetMessage("SalvageAdmin", player.UserIDString));
}
else
{
SpawnTugboatRaider();
player.ChatMessage(GetMessage("TugboatAdmin", player.UserIDString));
}
}
private void SpawnTugboatRaider()
{
// Find a spot in the ocean
Vector3 pos = FindOceanPosition();
if (pos == Vector3.zero) return;
var tugboat = GameManager.server.CreateEntity("assets/content/vehicles/boats/tugboat/tugboat.prefab", pos) as Tugboat;
if (tugboat == null) return;
tugboat.Spawn();
_activePirateEntities.Add(tugboat);
// Add a hackable crate to the deck
var crate = GameManager.server.CreateEntity("assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate.prefab", tugboat.transform.position + (Vector3.up * 3)) as HackableLockedCrate;
if (crate != null)
{
crate.Spawn();
crate.SetParent(tugboat, true, true);
_activePirateEntities.Add(crate);
}
// Spawn some scientists as pirates
for (int i = 0; i < 3; i++)
{
var pirate = GameManager.server.CreateEntity("assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_heavy.prefab", tugboat.transform.position + Vector3.up) as HumanNPC;
if (pirate != null)
{
pirate.Spawn();
pirate.SetParent(tugboat, true, true);
_activePirateEntities.Add(pirate);
}
}
// Spawn a BLUE map marker for the tugboat raider
SpawnMapMarker(pos, new Color(0.2f, 0.4f, 1f, 1f), new Color(0f, 0.1f, 0.4f, 0.5f), 0.12f, "Pirate Tugboat");
PrintToChat(GetMessage("TugboatSpawned", null));
Puts(GetMessage("LogTugboat", null, pos));
}
private void SpawnDeepSeaSalvage()
{
Vector3 pos = FindOceanPosition();
if (pos == Vector3.zero) return;
Vector3 markerPos = pos; // Save surface position for marker
// Anchor it to the sea floor
pos.y = TerrainMeta.WaterMap.GetHeight(pos) - 20; // Deepish
var crate = GameManager.server.CreateEntity("assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate.prefab", pos) as HackableLockedCrate;
if (crate == null) return;
crate.Spawn();
_activePirateEntities.Add(crate);
// Spawn a CYAN map marker for deep sea salvage
SpawnMapMarker(markerPos, new Color(0f, 0.9f, 0.9f, 1f), new Color(0f, 0.3f, 0.5f, 0.5f), 0.08f, "Deep Sea Salvage");
PrintToChat(GetMessage("SalvageSpawned", null));
Puts(GetMessage("LogSalvage", null, markerPos));
}
private Vector3 FindOceanPosition()
{
for (int i = 0; i < 10; i++)
{
float x = UnityEngine.Random.Range(-TerrainMeta.Size.x / 2, TerrainMeta.Size.x / 2);
float z = UnityEngine.Random.Range(-TerrainMeta.Size.z / 2, TerrainMeta.Size.z / 2);
Vector3 pos = new Vector3(x, 0, z);
if ((TerrainMeta.TopologyMap.GetTopology(pos) & (int)TerrainTopology.Enum.Ocean) != 0)
{
pos.y = TerrainMeta.WaterMap.GetHeight(pos);
return pos;
}
}
return Vector3.zero;
}
#endregion
}
}