C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core.Plugins;
using Oxide.Game.Rust;
using UnityEngine;
namespace Oxide.Plugins
{
// Lets players change their mounted horse's visual breed (0-9) via chat or admin console.
// Optional payment: ServerRewards RP, Economics money, a configurable item, or free if all are disabled.
[Info("Horse Breeds", "iLakSkiL", "1.2.1")]
[Description("Change the breed of your horse.")]
public class HorseBreeds : RustPlugin
{
// Soft dependencies — only used when the matching config flag is true.
[PluginReference]
private Plugin ServerRewards, Economics;
#region Configuration
private Configuration _config;
// Maps to oxide/config/HorseBreeds.json. Only one currency mode should be enabled at a time.
public class Configuration
{
[JsonProperty(PropertyName = "Use ServerRewards")]
public bool currencySR = false;
[JsonProperty(PropertyName = "Use Economics")]
public bool currencyEC = false;
[JsonProperty(PropertyName = "Use Item as Currency")]
public bool currencyItem = false;
// Item shortname (e.g. "scrap") when currencyItem is true.
[JsonProperty(PropertyName = "Item Short Name")]
public string itemToUse = "scrap";
[JsonProperty(PropertyName = "Horse Breed Costs")]
public Costs costs = new Costs();
// cost0-cost9 align with RidableHorse breed indices (see /horse help).
public class Costs
{
[JsonProperty(PropertyName = "Dapple Grey")]
public double cost4 = 20;
[JsonProperty(PropertyName = "Red Roan")]
public double cost7 = 20;
[JsonProperty(PropertyName = "Appalosa")]
public double cost0 = 30;
[JsonProperty(PropertyName = "Bay")]
public double cost1 = 30;
[JsonProperty(PropertyName = "Buckskin")]
public double cost2 = 30;
[JsonProperty(PropertyName = "Pinto")]
public double cost6 = 30;
[JsonProperty(PropertyName = "Chestnut")]
public double cost3 = 40;
[JsonProperty(PropertyName = "Piebald")]
public double cost5 = 40;
[JsonProperty(PropertyName = "White Thoroughbred")]
public double cost8 = 50;
[JsonProperty(PropertyName = "Black Thoroughbred")]
public double cost9 = 50;
}
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null)
throw new JsonException("Config deserialized to null.");
SaveConfig();
}
catch (Exception ex)
{
// Fall back so commands never run with a null _config reference.
PrintError($"Error reading config: {ex.Message}. Loading default config.");
LoadDefaultConfig();
}
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
Puts("Loading Default Config");
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config);
#endregion
#region Hooks
private void Loaded()
{
// horsebreeds.use — required for /horse
// horsebreeds.bypass — skips payment (grant to admins/VIPs as needed)
permission.RegisterPermission("horsebreeds.use", this);
permission.RegisterPermission("horsebreeds.bypass", this);
if (_config == null)
{
LoadDefaultConfig();
return;
}
// Enforce a single payment method; conflicting flags reset the config.
if ((_config.currencySR && _config.currencyEC) || (_config.currencySR && _config.currencyItem) || (_config.currencyItem && _config.currencyEC))
{
Puts("ERROR: Cannot use more than one currency type. Default config loaded.");
LoadDefaultConfig();
}
if (_config.currencyItem && string.IsNullOrWhiteSpace(_config.itemToUse))
{
Puts("ERROR: Item Short Name listed as currency is not valid. Default config loaded.");
LoadDefaultConfig();
}
}
#endregion
#region Commands
// In-game: /horse help | /horse <breed name or 0-9>
[ChatCommand("horse")]
private void horseChatCmd(BasePlayer player, string command, string[] args)
{
if (player == null)
return;
if (!permission.UserHasPermission(player.UserIDString, "horsebreeds.use"))
{
Player.Message(player, "Horse Breeds:You do not have permission to use this command.");
return;
}
// Exactly one argument: either "help" or a breed identifier.
if (args == null || args.Length != 1)
{
Player.Message(player, "Horse Breeds: Syntax Error. Use \"/horse help\" for more info.");
return;
}
if (args[0].Equals("help", StringComparison.OrdinalIgnoreCase))
{
SendHelp(player);
return;
}
if (!TryGetMountedHorse(player, out var horse))
{
Player.Message(player, "Horse Breeds:You must be on a horse to use this command.");
return;
}
if (!IsValidBreed(args[0], out int breed, out string breedName, out double cost))
{
Player.Message(player, "Horse Breeds: Invalid Breed. Use \"/horse help\" for more info.");
return;
}
if (permission.UserHasPermission(player.UserIDString, "horsebreeds.bypass"))
cost = 0;
ChangeHorse(player, horse, breed, breedName, cost);
}
// Server console or admin client: horse <player> <breed>
[ConsoleCommand("horse")]
private void horseConsoleCmd(ConsoleSystem.Arg arg)
{
// arg.Connection == null means dedicated server console (always allowed).
if (arg.Connection != null && !arg.IsAdmin)
return;
if (arg.Args == null || arg.Args.Length != 2)
{
Puts("Syntax: horse <player> <breed>");
return;
}
var target = RustCore.FindPlayer(arg.Args[0].ToString());
if (target == null)
{
Puts($"Player '{arg.Args[0]}' not found");
return;
}
if (!target.IsConnected)
{
Puts($"Player '{arg.Args[0]}' is not online");
return;
}
if (!TryGetMountedHorse(target, out var horse))
{
Puts($"Player '{arg.Args[0]}' is not on a horse");
return;
}
if (!IsValidBreed(arg.Args[1].ToString(), out int breed, out string breedName, out double cost))
{
Puts("Invalid breed type");
return;
}
if (permission.UserHasPermission(target.UserIDString, "horsebreeds.bypass"))
cost = 0;
ChangeHorse(target, horse, breed, breedName, cost);
}
#endregion
#region Helpers
// Lists all breeds and prices; currency label reflects the active payment mode.
private void SendHelp(BasePlayer player)
{
string currency = "free";
if (_config.currencySR)
currency = "RP";
else if (_config.currencyEC)
currency = "Dollars";
else if (_config.currencyItem)
currency = _config.itemToUse;
Player.Message(player,
"Horse Breeds:\n\nTo change the breed of your horse, use \"/horse (breed)\" while sitting on a horse. (breed) can either be the name or the numerical value of the breed below.\n\n" +
"Breed # - Breed Name - Cost\n" +
$"0 - Appalosa - {_config.costs.cost0} {currency}\n" +
$"1 - Bay - {_config.costs.cost1} {currency}\n" +
$"2 - Buckskin - {_config.costs.cost2} {currency}\n" +
$"3 - Chestnut - {_config.costs.cost3} {currency}\n" +
$"4 - Dapple Grey - {_config.costs.cost4} {currency}\n" +
$"5 - Piebald - {_config.costs.cost5} {currency}\n" +
$"6 - Pinto - {_config.costs.cost6} {currency}\n" +
$"7 - Red Roan - {_config.costs.cost7} {currency}\n" +
$"8 - White Thoroughbred - {_config.costs.cost8} {currency}\n" +
$"9 - Black Thoroughbred - {_config.costs.cost9} {currency}");
}
// GetMountedVehicle returns the vehicle parent; horses are RidableHorse (not the old RidableHorse2 type).
private static bool TryGetMountedHorse(BasePlayer player, out RidableHorse horse)
{
horse = player.GetMountedVehicle() as RidableHorse;
return horse != null;
}
// currentBreedIndex is private on RidableHorse — compare breed assets via GetBreed() instead.
private static bool IsSameBreed(RidableHorse horse, int breedIndex)
{
if (horse.breeds == null || breedIndex < 0 || breedIndex >= horse.breeds.Length)
return false;
var current = horse.GetBreed();
return current != null && current == horse.breeds[breedIndex];
}
// Charges the player (if configured), then calls horse.SetBreed(index) on the game entity.
private void ChangeHorse(BasePlayer player, RidableHorse horse, int breed, string breedName, double cost)
{
if (horse == null)
{
Player.Message(player, "Horse Breeds:You must be on a horse to use this command.");
return;
}
if (IsSameBreed(horse, breed))
{
Player.Message(player, $"Horse Breeds: Your horse is already a {breedName}");
return;
}
// --- ServerRewards (RP points) ---
if (_config.currencySR)
{
if (ServerRewards == null)
{
Player.Message(player, "Horse Breeds: ServerRewards is not loaded.");
return;
}
var balanceObj = ServerRewards.Call("CheckPoints", player.userID);
if (balanceObj == null)
{
Player.Message(player, "Horse Breeds: Could not read ServerRewards balance.");
return;
}
int balance = Convert.ToInt32(balanceObj);
if (balance < cost)
{
Player.Message(player, "Horse Breeds: Insufficient balance");
return;
}
horse.SetBreed(breed);
if (cost > 0)
ServerRewards.Call("TakePoints", player.userID, (int)Math.Round(cost));
Player.Message(player, $"Horse Breeds: Your horse is now a {breedName}");
Puts($"{player.displayName}'s horse breed has been changed to {breedName}.");
return;
}
// --- Economics (money) ---
if (_config.currencyEC)
{
if (Economics == null)
{
Player.Message(player, "Horse Breeds: Economics is not loaded.");
return;
}
var balanceObj = Economics.Call("Balance", player.userID);
if (balanceObj == null)
{
Player.Message(player, "Horse Breeds: Could not read Economics balance.");
return;
}
double balance = Convert.ToDouble(balanceObj);
if (balance < cost)
{
Player.Message(player, "Horse Breeds: Insufficient balance");
return;
}
horse.SetBreed(breed);
if (cost > 0)
Economics.Call("Withdraw", player.userID, cost);
Player.Message(player, $"Horse Breeds: Your horse is now a {breedName}");
Puts($"{player.displayName}'s horse breed has been changed to {breedName}.");
return;
}
// --- Item currency (searches belt, main, wear via PlayerInventory) ---
if (_config.currencyItem)
{
var item = player.inventory.FindItemByItemName(_config.itemToUse);
if (item == null)
{
Player.Message(player, "Horse Breeds: Insufficient balance");
return;
}
int itemAmount = player.inventory.GetAmount(item.info.itemid);
int amountToTake = (int)Math.Round(cost);
if (amountToTake > itemAmount)
{
Player.Message(player, "Horse Breeds: Insufficient balance");
return;
}
var taken = new List<Item>();
int removed = player.inventory.Take(taken, item.info.itemid, amountToTake);
// Only apply breed if the full cost was removed (Take can partially fail).
if (removed < amountToTake)
{
Player.Message(player, "Horse Breeds: Insufficient balance");
return;
}
horse.SetBreed(breed);
Player.Message(player, $"Horse Breeds: Your horse is now a {breedName}");
Puts($"{player.displayName}'s horse breed has been changed to {breedName}.");
return;
}
// --- No currency enabled: free breed change ---
horse.SetBreed(breed);
Player.Message(player, $"Horse Breeds: Your horse is now a {breedName}");
Puts($"{player.displayName}'s horse breed has been changed to {breedName}.");
}
// Parses user input (number or name) into breed index, display name, and config cost.
private bool IsValidBreed(string option, out int breed, out string breedName, out double cost)
{
breed = -1;
breedName = null;
cost = 0;
if (string.IsNullOrWhiteSpace(option))
return false;
var key = option.Trim().ToLowerInvariant();
// Numeric shorthand: /horse 3
if (int.TryParse(key, out int index) && TryGetBreedByIndex(index, out breed, out breedName, out cost))
return true;
// Name aliases (exact match on normalized string to avoid false positives).
if (key == "appalosa" || key == "appaloosa")
return TryGetBreedByIndex(0, out breed, out breedName, out cost);
if (key == "bay")
return TryGetBreedByIndex(1, out breed, out breedName, out cost);
if (key == "buckskin")
return TryGetBreedByIndex(2, out breed, out breedName, out cost);
if (key == "chestnut")
return TryGetBreedByIndex(3, out breed, out breedName, out cost);
if (key == "dapple" || key == "dapple grey" || key == "dapplegray" || key == "dapple gray")
return TryGetBreedByIndex(4, out breed, out breedName, out cost);
if (key == "piebald")
return TryGetBreedByIndex(5, out breed, out breedName, out cost);
if (key == "pinto")
return TryGetBreedByIndex(6, out breed, out breedName, out cost);
if (key == "red roan" || key == "redroan")
return TryGetBreedByIndex(7, out breed, out breedName, out cost);
if (key == "white thoroughbred" || key == "white")
return TryGetBreedByIndex(8, out breed, out breedName, out cost);
if (key == "black thoroughbred" || key == "black")
return TryGetBreedByIndex(9, out breed, out breedName, out cost);
return false;
}
// Maps game breed index 0-9 to display name and cost from config.
private bool TryGetBreedByIndex(int index, out int breed, out string breedName, out double cost)
{
breed = index;
breedName = null;
cost = 0;
switch (index)
{
case 0:
breedName = "Appalosa";
cost = _config.costs.cost0;
return true;
case 1:
breedName = "Bay";
cost = _config.costs.cost1;
return true;
case 2:
breedName = "Buckskin";
cost = _config.costs.cost2;
return true;
case 3:
breedName = "Chestnut";
cost = _config.costs.cost3;
return true;
case 4:
breedName = "Dapple Grey";
cost = _config.costs.cost4;
return true;
case 5:
breedName = "Piebald";
cost = _config.costs.cost5;
return true;
case 6:
breedName = "Pinto";
cost = _config.costs.cost6;
return true;
case 7:
breedName = "Red Roan";
cost = _config.costs.cost7;
return true;
case 8:
breedName = "White Thoroughbred";
cost = _config.costs.cost8;
return true;
case 9:
breedName = "Black Thoroughbred";
cost = _config.costs.cost9;
return true;
default:
return false;
}
}
#endregion
}
}