C#:
/*
* Copyright (c) 2026 Bazz3l
* Author Bazz3l
* Discord bazz3l
*
* Monument Helper cannot be copied, edited and/or (re)distributed without the express permission of the Author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
using System.Collections.Generic;
using System.IO;
using System;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Configuration;
using Oxide.Core;
using Newtonsoft.Json;
using UnityEngine;
namespace Oxide.Plugins;
[Info("Monument Helper", "Bazz3l", "1.0.4")]
[Description("")]
internal class MonumentHelper : CovalencePlugin
{
#region Fields
private const string PERM_USE = "monumenthelper.use";
private BaseStorage<PlayerData> _playerData;
private PluginConfig _config;
private static Dictionary<string, string> GetMonumentDetails()
{
return new Dictionary<string, string>
{
{"Harbor", "Low-tier loot and a recycler can be found here."},
{"Fishing Village", "A safe zone to fish, purchase items and vehicles for the water."},
{"Large Fishing Village", "A safe zone to fish, purchase items and vehicles for the water."},
{"Abandoned Military Base", "Medium-value loot and key cards can be found here. Watch out for armed scientists."},
{"Arctic Research Base", "Medium-value loot, key cards and snowmobiles can be found here. Watch out for armed scientists"},
{"Bandit Camp<br>", "Here you can gamble scrap, recycle items and purchase goods. Be careful not to inflict damage around these parts."},
{"Outpost", "Here you can gamble scrap, recycle items and purchase goods. Be careful not to inflict damage around these parts."},
{"Junkyard", "A useful place to find loot and crush vehicles."},
{"Ranch", "Yeehaw Cowboy!! Here you can get yourself a fast set of legs."},
{"Military Tunnel", "High-value loot can be found in this area. Watch out for armed scientists and radiation."},
{"Train Yard", "Medium-value loot can be found in this area. Watch out for armed scientists."},
{"Sulfur Quarry", "Place low grade fuel in the quarry to start mining sulfur."},
{"Satellite Dish", "A useful monument for loot, key cards and recycling items."},
{"HQM Quarry", "Place low-grade fuel in the quarry to start mining high-quality metal."},
{"The Dome", "Loot can be found at the top, but watch your step its a long way down."},
{"Stone Quarry", "Place low-grade fuel in the quarry to start mining stone."},
{"Water Well", "Feeling a little parched? Pump the well to quench your thirst."},
{"Abandoned Cabins", "Nobody knows what happened to the people that lived here."},
{"Wild Swamp", "Murky water... be sure to filter the water before drinking."},
{"Ice Lake", ""},
{"Train Tunnel", "Medium-value loot can be found in this area. Watch out for armed scientists."},
{"Abandoned Supermarket", "A useful place to find loot, food, recycle items and find key cards to progress."},
{"Mining Outpost", "Low-value loot and a recycler can be found here."},
{"Oxum's Gas Station", "A useful place to find loot, food, recycle items and find key cards to progress."},
{"Substation", ""},
{"Underwater Lab", "High-value loot can be found in this area. Watch out for armed scientists."},
{"Oil Rig", "High-value loot can be found in this area. Watch out for armed scientists."},
{"Large Oil Rig", "High-value loot can be found in this area. Watch out for armed scientists."},
{"Lighthouse", "Low-value loot, key cards and a recycler can be found here."},
{"Ferry Terminal", "A coastal monument with crates, barrels and recycler. No radiation or scientists."},
{"Launch Site", "High-value loot, key cards, and blueprints are available. Armed scientists and radiation make this location dangerous."},
{"Missile Silo", "High-tier loot and blueprints inside a highly fortified structure. Expect armed scientists."}
};
}
private static List<string> GetBlacklistDefaults()
{
return new List<string>
{
"Substation",
"Ice Lake"
};
}
#endregion
#region Lang
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
{ LangKeys.PREFIX, "<color=#3669CF>Monument Helper</color>: type /mh to toggle on and off.<br>" },
{ LangKeys.MONUMENT_ENTER, "You have entered <color=#3BA55C>{0}</color>.<br>{1}" },
{ LangKeys.MONUMENT_LEAVE, "You left <color=#ED4245>{0}</color>" },
{ LangKeys.TOGGLE_ENABLED, "<color=#3BA55C>Enabled</color>" },
{ LangKeys.TOGGLE_DISABLE, "<color=#ED4245>Disabled</color>" },
{ LangKeys.TOGGLED, "was {0}" },
}, this);
}
private static class LangKeys
{
public const string PREFIX = "Prefix";
public const string MONUMENT_LEAVE = "MonumentLeave";
public const string MONUMENT_ENTER = "MonumentEnter";
public const string TOGGLE_ENABLED = "ToggleEnabled";
public const string TOGGLE_DISABLE = "ToggleDisable";
public const string TOGGLED = "Toggled";
}
private void MessagePlayer(IPlayer? player, string langKey, params object[] args)
{
if (player == null || !player.IsConnected) return;
string message = string.Format(lang.GetMessage(langKey, this, player.Id), args);
player.Message(_config.EnablePrefix ? string.Format(lang.GetMessage(LangKeys.PREFIX, this, player.Id) + message) : message);
}
#endregion
#region Config
protected override void LoadDefaultConfig() => _config = PluginConfig.DefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<PluginConfig>() ?? throw new JsonException();
}
catch(Exception e)
{
LoadDefaultConfig();
PrintWarning("Loaded default config: {0}", e.Message);
}
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
private class PluginConfig
{
[JsonProperty("Chat command")]
public string Command { get; set; }
[JsonProperty("Display message prefix")]
public bool EnablePrefix { get; set; }
[JsonProperty("Display message when players enter a monument")]
public bool EnableEnter { get; set; }
[JsonProperty("Display message when players a leave monument")]
public bool EnableLeave { get; set; }
[JsonProperty("Wipe player toggles on new map save")]
public bool EnableWipeToggles { get; set; }
[JsonProperty("Monuments")]
public Dictionary<string, MonumentConfig> Monuments;
public static PluginConfig DefaultConfig()
{
return new PluginConfig
{
Command = "mh",
EnablePrefix = true,
EnableEnter = true,
EnableLeave = true,
Monuments = new Dictionary<string, MonumentConfig>()
};
}
}
private class MonumentConfig
{
public string DisplayName { get; set; }
public string Description { get; set; }
public bool Enabled { get; set; }
public Vector3 Center { get; set; }
public Vector3 Bounds { get; set; }
private string[]? _messageArgs;
[JsonIgnore]
public string[] MessageArgs
{
get
{
_messageArgs ??= new[] { DisplayName, Description };
return _messageArgs;
}
}
public static MonumentConfig New(bool enabled, string displayName, string description, Vector3 center, Vector3 bounds)
{
return new MonumentConfig
{
Enabled = enabled,
DisplayName = displayName,
Description = description,
Center = center,
Bounds = bounds
};
}
}
#endregion
#region Storage
private class BaseStorage<T> where T : new()
{
private DynamicConfigFile _cfg;
public T Data;
public bool IsDirty;
public void Load(params string[] paths)
{
string filepath = Path.Combine(paths);
_cfg = Interface.Oxide.DataFileSystem.GetFile(filepath);
try
{
Data = _cfg.ReadObject<T>() ?? throw new Exception();
}
catch
{
Data = new T();
Interface.Oxide.LogDebug("[BankHeist] - Failed to load {0}; loaded default data.", filepath);
}
}
public void Save()
{
if (!IsDirty)
return;
IsDirty = false;
_cfg.WriteObject(Data, true);
}
}
private class PlayerData
{
public HashSet<string> PlayerToggles = new();
public bool HasToggled(string userID) => PlayerToggles.Contains(userID);
public string Toggled(string userID)
{
if (!PlayerToggles.Remove(userID))
{
PlayerToggles.Add(userID);
return LangKeys.TOGGLE_DISABLE;
}
return LangKeys.TOGGLE_ENABLED;
}
}
private void LoadData()
{
_playerData = new BaseStorage<PlayerData>();
_playerData.Load(Name, "PlayerData");
}
private void SaveData()
{
if (_playerData != null)
_playerData.Save();
}
private void WipeData()
{
if (_playerData == null)
return;
_playerData.Data.PlayerToggles.Clear();
_playerData.IsDirty = true;
_playerData.Save();
}
#endregion
#region Oxide Hooks
private void OnServerInitialized()
{
AddCovalenceCommand(_config.Command, nameof(ToggleCommand), PERM_USE);
UpdateAndCreate();
}
private void Init()
{
permission.RegisterPermission(PERM_USE, this);
LoadData();
}
private void Unload()
{
MonumentTrigger.OnUnload();
SaveData();
}
private void OnNewSave(string filename)
{
if (!_config.EnableWipeToggles)
return;
WipeData();
}
#endregion
#region Trigger Management
private class MonumentTrigger : TriggerBase
{
public static readonly List<MonumentTrigger> MonumentTriggers = new();
public Func<BaseNetworkable, string[], bool> OnEnter;
public Func<BaseNetworkable, string[], bool> OnLeave;
public MonumentConfig MonumentConfig;
public void Initialize()
{
OnEntityEnterTrigger = (networkable) => OnEnter(networkable, MonumentConfig.MessageArgs);
OnEntityLeaveTrigger = (networkable) => OnLeave(networkable, MonumentConfig.MessageArgs);
}
public void Destroy()
{
OnEntityEnterTrigger = null;
OnEntityLeaveTrigger = null;
MonumentTrigger.DestroyMonumentTrigger(this);
}
public override GameObject? InterestedInObject(GameObject obj)
{
obj = base.InterestedInObject(obj);
if (obj == null) return null;
BasePlayer? baseEntity = obj.ToBaseEntity() as BasePlayer;
return baseEntity != null && baseEntity.isServer ? baseEntity.gameObject : null;
}
public static void OnUnload()
{
if (Rust.Application.isQuitting)
return;
foreach (MonumentTrigger trigger in new List<MonumentTrigger>(MonumentTriggers))
trigger.Destroy();
}
public static void DestroyMonumentTrigger(MonumentTrigger monumentTrigger)
{
if (monumentTrigger == null)
return;
DestroyImmediate(monumentTrigger?.gameObject);
}
}
private void UpdateAndCreate()
{
bool hasChanged = false;
Dictionary<string, string> monumentDetails = GetMonumentDetails();
List<string> blockedList = GetBlacklistDefaults();
foreach (MonumentInfo monumentInfo in TerrainMeta.Path.Monuments)
{
string? displayName = monumentInfo?.displayPhrase?.english;
if (string.IsNullOrEmpty(displayName) || blockedList.Contains(displayName))
continue;
if (!_config.Monuments.TryGetValue(displayName, out MonumentConfig? monumentConfig))
{
monumentDetails.TryGetValue(displayName, out string? description);
monumentConfig = MonumentConfig.New(monumentInfo.isActiveAndEnabled, displayName, description ?? string.Empty, monumentInfo.Bounds.center, monumentInfo.Bounds.size);
_config.Monuments[displayName] = monumentConfig;
hasChanged = true;
}
if (monumentConfig == null)
continue;
CreateMonumentTrigger(monumentInfo.transform, monumentConfig);
}
if (!hasChanged)
return;
SaveConfig();
}
private void CreateMonumentTrigger(Transform transform, MonumentConfig config)
{
if (!config.Enabled || config.Bounds == Vector3.zero)
return;
GameObject gameObject = new GameObject("Monument_Trigger");
gameObject.layer = (int)Rust.Layer.Trigger;
gameObject.transform.SetPositionAndRotation(transform.position, transform.rotation);
BoxCollider collider = gameObject.AddComponent<BoxCollider>();
collider.isTrigger = true;
collider.center = config.Center;
collider.size = config.Bounds;
MonumentTrigger trigger = gameObject.AddComponent<MonumentTrigger>();
trigger.MonumentConfig = config;
if (_config.EnableEnter) trigger.OnEnter = OnMonumentTriggerEnter;
if (_config.EnableLeave) trigger.OnLeave = OnMonumentTriggerLeave;
trigger.InterestLayers = Rust.Layers.Server.Players;
trigger.Initialize();
MonumentTrigger.MonumentTriggers.Add(trigger);
}
private bool OnMonumentTriggerEnter(BaseNetworkable networkable, string[] message)
{
BasePlayer? player = networkable as BasePlayer;
if (player == null || player.IsNpc || player.IPlayer == null)
return false;
if (!player.IPlayer.HasPermission(PERM_USE))
return false;
if (_playerData.Data.HasToggled(player.UserIDString))
return false;
MessagePlayer(player.IPlayer, LangKeys.MONUMENT_ENTER, message[0], message[1]);
return true;
}
private bool OnMonumentTriggerLeave(BaseNetworkable networkable, string[] message)
{
BasePlayer? player = networkable as BasePlayer;
if (player == null || player.IsNpc || player.IPlayer == null)
return false;
if (!player.IPlayer.HasPermission(PERM_USE))
return false;
if (_playerData.Data.HasToggled(player.UserIDString))
return false;
MessagePlayer(player.IPlayer, LangKeys.MONUMENT_LEAVE, message[0]);
return true;
}
#endregion
#region Chat Command
private void ToggleCommand(IPlayer player, string command, string[] args)
{
string option = _playerData.Data.Toggled(player.Id);
MessagePlayer(player, LangKeys.TOGGLED, lang.GetMessage(option, this, player.Id));
}
#endregion
}