C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("CannonsOnBases", "wGw", "3.0.1")]
[Description("Place real boat cannons on base floors/foundations like auto-turrets. Left-click while holding the cannon item. E to mount / long-press E to pick up.")]
public class CannonsOnBases : RustPlugin
{
private Configuration _config;
private StoredData _data;
private readonly Dictionary<ulong, float> _lastPlaceTime = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, float> _useHoldStart = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, BaseEntity> _useHoldTarget = new Dictionary<ulong, BaseEntity>();
private const string PrefabCannon = "assets/prefabs/deployable/boatbuilding/cannon/cannon.deployed.prefab";
private const string ItemShortname = "cannon";
private const string DeployedShortname = "cannon.deployed";
private const string PermPlace = "cannonsonbases.place";
private const string PermAdmin = "cannonsonbases.admin";
private const int BuildLayerMask = 1219701497;
private const float PlaceDistance = 4f;
private const float MountDistance = 5f;
private const float FlatNormalThreshold = 0.85f;
private const float LongPressSeconds = 0.75f;
private class Configuration
{
[JsonProperty("Max Cannons Per Tool Cupboard")]
public int MaxCannonsPerTC { get; set; } = 8;
[JsonProperty("Only On Owned Blocks")]
public bool OnlyOnOwnedBlocks { get; set; } = true;
[JsonProperty("Place Cooldown Seconds")]
public float PlaceCooldownSeconds { get; set; } = 3f;
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null) throw new Exception();
}
catch
{
PrintError("Config corrupted or missing - loading defaults.");
LoadDefaultConfig();
}
if (_config.MaxCannonsPerTC < 1) _config.MaxCannonsPerTC = 8;
if (_config.PlaceCooldownSeconds < 0.5f) _config.PlaceCooldownSeconds = 3f;
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
private class StoredData
{
public Dictionary<ulong, int> TcCannonCount { get; set; } = new Dictionary<ulong, int>();
public Dictionary<ulong, ulong> CannonToTc { get; set; } = new Dictionary<ulong, ulong>();
}
private void LoadData()
{
_data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
}
private void SaveData()
{
if (_data == null) return;
Interface.Oxide.DataFileSystem.WriteObject(Name, _data);
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "No permission.",
["ReloadSuccess"] = "CannonsOnBases config & data reloaded.",
["RecountDone"] = "Recount complete. {0} cannons tracked."
}, this, "en");
}
private string Lang(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
private void Init()
{
permission.RegisterPermission(PermPlace, this);
permission.RegisterPermission(PermAdmin, this);
LoadData();
}
private void OnServerInitialized()
{
ReconcileCannons();
}
private void OnServerSave() => SaveData();
private void Unload()
{
SaveData();
_lastPlaceTime.Clear();
_useHoldStart.Clear();
_useHoldTarget.Clear();
_data = null;
_config = null;
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (player == null) return;
_lastPlaceTime.Remove(player.userID);
_useHoldStart.Remove(player.userID);
_useHoldTarget.Remove(player.userID);
}
private void OnPlayerInput(BasePlayer player, InputState input)
{
if (player == null || !player.IsConnected || player.IsDead() || player.IsSleeping()) return;
if (input.WasJustPressed(BUTTON.FIRE_PRIMARY))
{
var held = player.GetHeldEntity() as HeldEntity;
if (held == null) return;
var item = held.GetItem();
if (item == null || item.info == null || item.info.shortname != ItemShortname || item.amount < 1)
return;
TryPlaceCannon(player, item);
return;
}
if (input.WasJustPressed(BUTTON.USE))
{
HandleUsePressed(player);
}
else if (input.IsDown(BUTTON.USE))
{
HandleUseHeld(player);
}
else if (input.WasJustReleased(BUTTON.USE))
{
HandleUseReleased(player);
}
}
private object CanPickupEntity(BasePlayer player, BaseEntity entity)
{
// Own the pickup so tracking stays correct. Engine "failed to pickup" log is boat-prefab noise.
if (entity != null && entity.ShortPrefabName == DeployedShortname)
return false;
return null;
}
private void OnEntityKill(BaseNetworkable entity)
{
if (entity == null || entity.net == null) return;
if (entity.ShortPrefabName != DeployedShortname) return;
ulong cannonId = entity.net.ID.Value;
if (_data.CannonToTc.TryGetValue(cannonId, out ulong tcId))
{
if (_data.TcCannonCount.TryGetValue(tcId, out int count) && count > 0)
{
count--;
if (count <= 0)
_data.TcCannonCount.Remove(tcId);
else
_data.TcCannonCount[tcId] = count;
}
_data.CannonToTc.Remove(cannonId);
}
}
private void HandleUsePressed(BasePlayer player)
{
var entity = GetLookedAtCannon(player);
if (entity == null)
{
ClearUseHold(player);
return;
}
_useHoldStart[player.userID] = Time.realtimeSinceStartup;
_useHoldTarget[player.userID] = entity;
}
private void HandleUseHeld(BasePlayer player)
{
if (!_useHoldStart.TryGetValue(player.userID, out float start))
return;
if (Time.realtimeSinceStartup - start < LongPressSeconds)
return;
if (!_useHoldTarget.TryGetValue(player.userID, out BaseEntity target) || target == null || target.IsDestroyed)
{
ClearUseHold(player);
return;
}
var current = GetLookedAtCannon(player);
if (current == null || current.net == null || target.net == null || current.net.ID != target.net.ID)
{
ClearUseHold(player);
return;
}
bool canPickup = target.OwnerID == player.userID ||
player.IsAdmin ||
permission.UserHasPermission(player.UserIDString, PermAdmin);
if (!canPickup)
{
ClearUseHold(player);
return;
}
DoPickupCannon(player, target);
ClearUseHold(player);
}
private void HandleUseReleased(BasePlayer player)
{
if (!_useHoldStart.TryGetValue(player.userID, out float start))
{
ClearUseHold(player);
return;
}
float held = Time.realtimeSinceStartup - start;
ClearUseHold(player);
if (held < LongPressSeconds)
TryForceMount(player);
}
private void ClearUseHold(BasePlayer player)
{
if (player == null) return;
_useHoldStart.Remove(player.userID);
_useHoldTarget.Remove(player.userID);
}
private BaseEntity GetLookedAtCannon(BasePlayer player)
{
Ray ray = player.eyes.HeadRay();
RaycastHit hit;
if (!Physics.Raycast(ray, out hit, MountDistance)) return null;
var entity = hit.GetEntity();
if (entity == null || entity.ShortPrefabName != DeployedShortname) return null;
return entity;
}
private void DoPickupCannon(BasePlayer player, BaseEntity entity)
{
if (player == null || entity == null || entity.IsDestroyed) return;
Item item = ItemManager.CreateByName(ItemShortname, 1);
if (item == null)
{
PrintWarning("Failed to create cannon item on pickup.");
return;
}
player.GiveItem(item);
entity.Kill();
Puts($"{player.displayName} ({player.userID}) picked up a base cannon.");
}
private void TryForceMount(BasePlayer player)
{
var entity = GetLookedAtCannon(player);
if (entity == null) return;
var mountable = entity as BaseMountable;
if (mountable == null || mountable.IsMounted()) return;
var block = entity.GetParentEntity() as BuildingBlock;
if (block == null)
block = entity.GetComponentInParent<BuildingBlock>();
if (block != null)
{
var tc = block.GetBuildingPrivilege();
if (tc != null && !tc.IsAuthed(player) && !player.IsAdmin)
return;
}
mountable.MountPlayer(player);
}
private void TryPlaceCannon(BasePlayer player, Item item)
{
float now = Time.realtimeSinceStartup;
if (_lastPlaceTime.TryGetValue(player.userID, out float last) && now - last < _config.PlaceCooldownSeconds)
return;
_lastPlaceTime[player.userID] = now;
if (!permission.UserHasPermission(player.UserIDString, PermPlace) &&
!permission.UserHasPermission(player.UserIDString, PermAdmin))
return;
Ray ray = player.eyes.HeadRay();
RaycastHit hit;
if (!GamePhysics.Trace(ray, 0f, out hit, PlaceDistance, BuildLayerMask, QueryTriggerInteraction.Collide))
return;
var block = hit.GetEntity() as BuildingBlock;
if (block == null || block.IsDestroyed) return;
if (hit.normal.y < FlatNormalThreshold)
return;
string shortName = block.ShortPrefabName ?? string.Empty;
if (!shortName.Contains("floor") && !shortName.Contains("foundation") &&
!shortName.Contains("floor.triangle") && !shortName.Contains("foundation.triangle"))
return;
var tc = block.GetBuildingPrivilege();
if (tc == null) return;
if (_config.OnlyOnOwnedBlocks && !tc.IsAuthed(player) && !player.IsAdmin)
return;
ulong tcId = tc.net.ID.Value;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
if (_data.TcCannonCount.TryGetValue(tcId, out int count) && count >= _config.MaxCannonsPerTC)
return;
}
Vector3 pos = hit.point + hit.normal * 0.05f;
Vector3 aimDir = Vector3.ProjectOnPlane(player.eyes.BodyForward(), hit.normal).normalized;
if (aimDir.sqrMagnitude < 0.01f)
aimDir = -hit.normal;
Quaternion rot = Quaternion.LookRotation(aimDir, hit.normal);
var cannonEntity = GameManager.server.CreateEntity(PrefabCannon, pos, rot);
if (cannonEntity == null)
{
PrintWarning("Failed to create cannon entity - prefab missing?");
return;
}
cannonEntity.OwnerID = player.userID;
cannonEntity.SetParent(block, true, true);
cannonEntity.Spawn();
NextTick(() =>
{
if (cannonEntity != null && !cannonEntity.IsDestroyed)
cannonEntity.SendNetworkUpdateImmediate();
});
if (!_data.TcCannonCount.ContainsKey(tcId))
_data.TcCannonCount[tcId] = 0;
_data.TcCannonCount[tcId]++;
_data.CannonToTc[cannonEntity.net.ID.Value] = tcId;
item.UseItem(1);
Puts($"{player.displayName} ({player.userID}) placed cannon on TC {tcId}");
}
private void ReconcileCannons()
{
if (_data == null) return;
_data.TcCannonCount.Clear();
_data.CannonToTc.Clear();
int found = 0;
foreach (var ent in BaseNetworkable.serverEntities)
{
if (ent == null || ent.IsDestroyed || ent.ShortPrefabName != DeployedShortname) continue;
if (ent.net == null) continue;
var block = ent.GetParentEntity() as BuildingBlock;
if (block == null) continue;
var tc = block.GetBuildingPrivilege();
if (tc == null || tc.net == null) continue;
ulong cannonId = ent.net.ID.Value;
ulong tcId = tc.net.ID.Value;
_data.CannonToTc[cannonId] = tcId;
if (!_data.TcCannonCount.ContainsKey(tcId))
_data.TcCannonCount[tcId] = 0;
_data.TcCannonCount[tcId]++;
found++;
}
SaveData();
Puts($"Reconciliation complete - {found} cannons tracked.");
}
[ChatCommand("cob")]
private void CmdCob(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin) && !player.IsAdmin)
return;
if (args == null || args.Length == 0)
{
player.ChatMessage("Usage: /cob reload | recount");
return;
}
switch (args[0].ToLower())
{
case "reload":
LoadConfig();
LoadData();
player.ChatMessage(Lang("ReloadSuccess", player.UserIDString));
break;
case "recount":
ReconcileCannons();
player.ChatMessage(Lang("RecountDone", player.UserIDString, _data.CannonToTc.Count));
break;
default:
player.ChatMessage("Usage: /cob reload | recount");
break;
}
}
[ConsoleCommand("cannonsonbases.reload")]
private void CmdReload(ConsoleSystem.Arg arg)
{
if (arg.Player() != null && !arg.Player().IsAdmin &&
!permission.UserHasPermission(arg.Player().UserIDString, PermAdmin))
return;
LoadConfig();
LoadData();
Puts("CannonsOnBases reloaded.");
}
}
}