C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using static ConversationData;
using static NPCTalking;
namespace Oxide.Plugins
{
[Info("Vehicle Vendor Options", "WhiteThunder", "1.7.10")]
[Description("Allows customizing vehicle fuel and prices at NPC vendors.")]
internal class VehicleVendorOptions : CovalencePlugin
{
#region Fields
[PluginReference]
private readonly Plugin Economics, ServerRewards;
private Configuration _config;
private const string Permission_Allow_All = "vehiclevendoroptions.allow.all";
private const string Permission_Free_All = "vehiclevendoroptions.free.allvehicles";
private const string Permission_Ownership_All = "vehiclevendoroptions.ownership.allvehicles";
private const string Permission_Price_Prefix = "vehiclevendoroptions.price";
private const int ScrapItemId = -932201673;
private const float VanillaDespawnProtectionTime = 300;
private readonly object False = false;
private Item _scrapItem;
private readonly VehicleInfoManager _vehicleInfoManager;
public VehicleVendorOptions()
{
_vehicleInfoManager = new VehicleInfoManager(this);
}
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(Permission_Allow_All, this);
permission.RegisterPermission(Permission_Free_All, this);
permission.RegisterPermission(Permission_Ownership_All, this);
_vehicleInfoManager.Init();
}
private void OnServerInitialized()
{
_scrapItem = ItemManager.CreateByItemID(ScrapItemId);
_vehicleInfoManager.OnServerInitialized();
}
private void Unload()
{
CostLabelUI.DestroyAll();
_scrapItem?.Remove();
}
private void OnEntitySpawned(HotAirBalloon vehicle) => HandleSpawn(vehicle);
private void OnEntitySpawned(PlayerHelicopter vehicle) => HandleSpawn(vehicle);
private void OnEntitySpawned(MotorRowboat vehicle) => HandleSpawn(vehicle);
private void OnEntitySpawned(BaseSubmarine vehicle) => HandleSpawn(vehicle);
private object OnRidableAnimalClaim(RidableHorse horse, BasePlayer player, Item saddleItem)
{
if (!horse.IsForSale)
return null;
var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(horse);
if (vehicleInfo == null)
return null;
if (!HasPermission(player.UserIDString, Permission_Free_All)
&& !HasPermission(player.UserIDString, vehicleInfo.FreePermission))
return null;
using var flagsScope = horse.StartSetFlags(BaseEntity.FlagsUpdateMode.SendNetworkUpdate_Flags);
flagsScope.Set(BaseEntity.Flags.Reserved2, false);
if (saddleItem != null)
{
horse.OnClaimedWithToken(saddleItem);
}
else
{
// If the saddle item is null, that means the hook is out of date, so just use one seat for now.
flagsScope.Set(BaseEntity.Flags.Reserved9, true);
flagsScope.Set(BaseEntity.Flags.Reserved10, false);
horse.UpdateMountFlags();
}
horse.AttemptMount(player, doMountChecks: false);
Interface.CallHook("OnRidableAnimalClaimed", horse, player);
return False;
}
private void OnRidableAnimalClaimed(RidableHorse horse, BasePlayer player)
{
SetOwnerIfPermission(horse, player);
}
private object OnNpcConversationRespond(VehicleVendor vendor, BasePlayer player, ConversationData conversationData, ResponseNode responseNode)
{
CostLabelUI.Destroy(player);
var resultingSpeechNode = ConversationUtils.FindSpeechNodeByGuid(conversationData, responseNode.resultingSpeechNode);
if (resultingSpeechNode == null)
return null;
var vehicleInfo = _vehicleInfoManager.GetForPayPrompt(resultingSpeechNode.shortname);
if (vehicleInfo != null)
{
// Player has selected a specific vehicle.
return HandlePayPrompt(vendor, player, conversationData, responseNode, resultingSpeechNode, vehicleInfo);
}
if (!string.IsNullOrEmpty(responseNode.actionString))
{
vehicleInfo = _vehicleInfoManager.GetForPayAction(responseNode.actionString);
if (vehicleInfo == null)
return null;
// Player has selected the option to pay for the vehicle.
return HandlePayment(vendor, player, conversationData, responseNode, vehicleInfo);
}
return null;
}
private void OnNpcConversationEnded(VehicleVendor vendor, BasePlayer player)
{
CostLabelUI.Destroy(player);
}
#endregion
#region Exposed Hooks
private static class ExposedHooks
{
// Standardized by ClaimVehicle.
public static void OnVehicleOwnershipChanged(BaseCombatEntity vehicle)
{
Interface.CallHook("OnVehicleOwnershipChanged", vehicle);
}
}
#endregion
#region Helpers
private static void AdjustFuel(VehicleSpawner.IVehicleSpawnUser vehicle, int desiredFuelAmount)
{
if (vehicle.GetFuelSystem() is not EntityFuelSystem fuelSystem)
return;
var fuelAmount = desiredFuelAmount < 0
? fuelSystem.GetFuelContainer().allowedItem.stackable
: desiredFuelAmount;
var fuelItem = fuelSystem.GetFuelItem();
if (fuelItem != null && fuelItem.amount != fuelAmount)
{
fuelItem.amount = fuelAmount;
fuelItem.MarkDirty();
}
}
private void HandleSpawn(VehicleSpawner.IVehicleSpawnUser vehicle)
{
if (Rust.Application.isLoadingSave)
return;
var vehicle2 = vehicle;
NextTick(() =>
{
var entity = vehicle2 as BaseCombatEntity;
if (entity?.creatorEntity == null)
return;
var vehicleConfig = _vehicleInfoManager.GetVehicleInfo(entity)?.VehicleConfig;
if (vehicleConfig == null)
return;
AdjustFuel(vehicle2, vehicleConfig.FuelAmount);
MaybeSetOwner(entity);
var spawnTimeDelta = vehicleConfig.DespawnProtectionSeconds - VanillaDespawnProtectionTime;
var baseVehicle = entity as BaseVehicle;
if ((object)baseVehicle != null)
{
baseVehicle.spawnTime += spawnTimeDelta;
}
var hotAirBalloon = entity as HotAirBalloon;
if ((object)hotAirBalloon != null)
{
hotAirBalloon.spawnTime += spawnTimeDelta;
}
});
}
private void MaybeSetOwner(BaseCombatEntity vehicle)
{
var basePlayer = vehicle.creatorEntity as BasePlayer;
if (basePlayer == null)
return;
SetOwnerIfPermission(vehicle, basePlayer);
}
private void SetOwnerIfPermission(BaseCombatEntity vehicle, BasePlayer basePlayer)
{
var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(vehicle);
if (vehicle == null)
return;
if (HasPermission(basePlayer.UserIDString, Permission_Ownership_All)
|| HasPermission( basePlayer.UserIDString, vehicleInfo.OwnershipPermission))
{
vehicle.OwnerID = basePlayer.userID;
ExposedHooks.OnVehicleOwnershipChanged(vehicle);
}
}
private bool HasPermission(string userIdString, string perm)
{
return permission.UserHasPermission(userIdString, perm);
}
private object HandlePayPrompt(VehicleVendor vendor, BasePlayer player, ConversationData conversationData,
ResponseNode responseNode, SpeechNodeData resultingSpeechNode, VehicleInfo vehicleInfo)
{
if (vehicleInfo.VehicleConfig.RequiresPermission
&& !HasPermission(player.UserIDString, Permission_Allow_All)
&& !HasPermission(player.UserIDString, vehicleInfo.PurchasePermission))
{
// End the conversation instead of showing the option to pay.
vendor.ForceEndConversation(player);
ChatMessage(player, "Error.Vehicle.NoPermission");
return False;
}
// Player has permission, so check if we need to modify the response options.
var scrapCondition = ConversationUtils.FindPayConditionInResponses(resultingSpeechNode, out var inverseCondition);
if (scrapCondition == null)
return null;
var vanillaPrice = (int)scrapCondition.conditionAmount;
var priceConfig = vehicleInfo.VehicleConfig.GetPriceForPlayer(this, player.IPlayer, vehicleInfo.FreePermission);
if (priceConfig == null || priceConfig.MatchesVanillaPrice(vanillaPrice))
return null;
// Always send the UI if the price is custom, regardless of whether the player has enough.
CostLabelUI.Create(this, player, priceConfig);
var playerScrapAmount = player.inventory.GetAmount(ScrapItemId);
var canAffordVanillaPrice = playerScrapAmount >= vanillaPrice;
var canAffordCustomPrice = priceConfig.CanPlayerAfford(player);
if (canAffordCustomPrice == canAffordVanillaPrice)
return null;
scrapCondition.conditionAmount = (uint)priceConfig.Amount;
if (inverseCondition != null)
{
inverseCondition.conditionAmount = (uint)priceConfig.Amount;
}
try
{
ConversationUtils.ForceSpeechNode(vendor, player, responseNode.resultingSpeechNode);
}
finally
{
scrapCondition.conditionAmount = (uint)vanillaPrice;
if (inverseCondition != null)
{
inverseCondition.conditionAmount = (uint)vanillaPrice;
}
}
Interface.CallHook("OnNpcConversationResponded", vendor, player, conversationData, responseNode);
return False;
}
// This method is mostly vanilla logic, with some changes to modify the price.
private object HandlePayment(VehicleVendor vendor, BasePlayer player, ConversationData conversationData, ResponseNode responseNode, VehicleInfo vehicleInfo)
{
if (responseNode.conditions.Length != 0)
{
vendor.UpdateFlags();
}
var scrapCondition = ConversationUtils.GetScrapCondition(responseNode);
if (scrapCondition == null)
return null;
var resultAction = ConversationUtils.FindResultAction(vendor, vehicleInfo.PayAction);
if (resultAction == null)
return null;
var vanillaPrice = (int)scrapCondition.conditionAmount;
var priceConfig = vehicleInfo.VehicleConfig.GetPriceForPlayer(this, player.IPlayer, vehicleInfo.FreePermission);
if (priceConfig == null || priceConfig.MatchesVanillaPrice(vanillaPrice))
return null;
if (!priceConfig.CanPlayerAfford(player))
{
vendor.ForceEndConversation(player);
return False;
}
if (priceConfig.RequiresScrap)
{
// Set the scrap price to the custom amount.
scrapCondition.conditionAmount = (uint)priceConfig.Amount;
resultAction.scrapCost = priceConfig.Amount;
}
else
{
// Set the scrap price to 0 since the player is being charged for custom currency.
priceConfig.TryChargePlayer(player);
scrapCondition.conditionAmount = 0;
resultAction.scrapCost = 0;
}
bool passesConditions;
try
{
passesConditions = responseNode.PassesConditions(player, vendor);
if (passesConditions && !string.IsNullOrEmpty(responseNode.actionString))
{
vendor.OnConversationAction(player, responseNode.actionString);
}
}
finally
{
// Revert scrap price to vanilla.
scrapCondition.conditionAmount = (uint)vanillaPrice;
resultAction.scrapCost = vanillaPrice;
}
var speechNodeIndex = conversationData.GetSpeechNodeIndex(passesConditions
? responseNode.resultingSpeechNode
: responseNode.GetFailedSpeechNode(player, vendor));
if (speechNodeIndex == -1)
{
vendor.ForceEndConversation(player);
return False;
}
vendor.ForceSpeechNode(player, conversationData, speechNodeIndex);
Interface.CallHook("OnNpcConversationResponded", this, player, conversationData, responseNode);
return False;
}
#endregion
#region Vehicle Info
private class VehicleInfo
{
public string PrefabPath;
public string PermissionSuffix;
public VehicleConfig VehicleConfig;
public string PayPrompt;
public string PayAction;
public uint PrefabId { get; private set; }
public string PurchasePermission { get; private set; }
public string OwnershipPermission { get; private set; }
public string FreePermission { get; private set; }
public void Init()
{
PurchasePermission = $"{nameof(VehicleVendorOptions)}.allow.{PermissionSuffix}".ToLower();
OwnershipPermission = $"{nameof(VehicleVendorOptions)}.ownership.{PermissionSuffix}".ToLower();
FreePermission = $"{nameof(VehicleVendorOptions)}.free.{PermissionSuffix}".ToLower();
}
public void OnServerInitialized()
{
var entity = GameManager.server.FindPrefab(PrefabPath)?.GetComponent<BaseEntity>();
if (entity != null)
{
PrefabId = entity.prefabID;
}
}
}
private class VehicleInfoManager
{
private readonly VehicleVendorOptions _plugin;
private readonly Dictionary<uint, VehicleInfo> _prefabIdToVehicleInfo = new();
private readonly Dictionary<string, VehicleInfo> _payPromptToVehicleInfo = new();
private readonly Dictionary<string, VehicleInfo> _payActionToVehicleInfo = new();
private VehicleInfo[] _allVehicles;
public VehicleInfoManager(VehicleVendorOptions plugin)
{
_plugin = plugin;
}
public void Init()
{
_allVehicles = new[]
{
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/minicopter/minicopter.entity.prefab",
PermissionSuffix = "minicopter",
VehicleConfig = _plugin._config.Vehicles.Minicopter,
PayPrompt = "minicopterbuy",
PayAction = "buyminicopter",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/scrap heli carrier/scraptransporthelicopter.prefab",
PermissionSuffix = "scraptransport",
VehicleConfig = _plugin._config.Vehicles.ScrapTransport,
PayPrompt = "transportbuy",
PayAction = "buytransport",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/attackhelicopter/attackhelicopter.entity.prefab",
PermissionSuffix = "attackhelicopter",
VehicleConfig = _plugin._config.Vehicles.AttackHelicopter,
PayPrompt = "attackbuy",
PayAction = "buyattack",
},
new VehicleInfo
{
PrefabPath = "assets/prefabs/deployable/hot air balloon/hotairballoon.prefab",
PermissionSuffix = "hotairballoon",
VehicleConfig = _plugin._config.Vehicles.HotAirBalloon,
PayPrompt = "habbuy",
PayAction = "buyhab",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/boats/rowboat/rowboat.prefab",
PermissionSuffix = "rowboat",
VehicleConfig = _plugin._config.Vehicles.Rowboat,
PayPrompt = "pay_rowboat",
PayAction = "buyboat",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/boats/rhib/rhib.prefab",
PermissionSuffix = "rhib",
VehicleConfig = _plugin._config.Vehicles.RHIB,
PayPrompt = "pay_rhib",
PayAction = "buyrhib",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/submarine/submarinesolo.entity.prefab",
PermissionSuffix = "solosub",
VehicleConfig = _plugin._config.Vehicles.SoloSub,
PayPrompt = "pay_sub",
PayAction = "buysub",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/submarine/submarineduo.entity.prefab",
PermissionSuffix = "duosub",
VehicleConfig = _plugin._config.Vehicles.DuoSub,
PayPrompt = "pay_duosub",
PayAction = "buysubduo",
},
new VehicleInfo
{
PrefabPath = "assets/content/vehicles/horse/ridablehorse.prefab",
PermissionSuffix = "ridablehorse",
},
};
foreach (var vehicleInfo in _allVehicles)
{
vehicleInfo.Init();
if (!string.IsNullOrEmpty(vehicleInfo.PayPrompt))
{
_payPromptToVehicleInfo[vehicleInfo.PayPrompt] = vehicleInfo;
}
if (!string.IsNullOrEmpty(vehicleInfo.PayAction))
{
_payActionToVehicleInfo[vehicleInfo.PayAction] = vehicleInfo;
}
}
// Register permissions in batches by permission type.
foreach (var vehicleInfo in _allVehicles)
{
_plugin.permission.RegisterPermission(vehicleInfo.PurchasePermission, _plugin);
}
foreach (var vehicleInfo in _allVehicles)
{
_plugin.permission.RegisterPermission(vehicleInfo.OwnershipPermission, _plugin);
}
foreach (var vehicleInfo in _allVehicles)
{
_plugin.permission.RegisterPermission(vehicleInfo.FreePermission, _plugin);
}
foreach (var vehicleInfo in _allVehicles)
{
vehicleInfo.VehicleConfig?.InitAndValidate(_plugin, vehicleInfo.PermissionSuffix);
}
}
public void OnServerInitialized()
{
foreach (var vehicleInfo in _allVehicles)
{
vehicleInfo.OnServerInitialized();
if (vehicleInfo.PrefabId != 0)
{
_prefabIdToVehicleInfo[vehicleInfo.PrefabId] = vehicleInfo;
}
else
{
_plugin.LogError($"Unable to determine Prefab ID for prefab: {vehicleInfo.PrefabPath}");
}
}
}
public VehicleInfo GetVehicleInfo(BaseEntity entity)
{
return _prefabIdToVehicleInfo.GetValueOrDefault(entity.prefabID);
}
public VehicleInfo GetForPayPrompt(string promptName)
{
return _payPromptToVehicleInfo.GetValueOrDefault(promptName);
}
public VehicleInfo GetForPayAction(string actionName)
{
return _payActionToVehicleInfo.GetValueOrDefault(actionName);
}
}
#endregion
#region Conversation Utilities
private static class ConversationUtils
{
public static ConversationCondition GetScrapCondition(ResponseNode responseNode, bool? inverse = null)
{
return FindCondition(responseNode, ConversationCondition.ConditionType.HasScrap, inverse);
}
public static ConversationCondition FindPayConditionInResponses(SpeechNodeData speechNode, out ConversationCondition inverseCondition)
{
ConversationCondition scrapCondition = null;
inverseCondition = null;
foreach (var futureResponseOption in speechNode.responses)
{
var foundScrapCondition = GetScrapCondition(futureResponseOption);
if (foundScrapCondition == null)
continue;
if (foundScrapCondition.inverse)
{
inverseCondition ??= foundScrapCondition;
}
else
{
scrapCondition ??= foundScrapCondition;
}
}
return scrapCondition;
}
public static void ForceSpeechNode(NPCTalking npcTalking, BasePlayer player, string speechNodeGuid)
{
var conversationData = npcTalking.GetConversationFor(player);
var speechNodeIndex = conversationData.GetSpeechNodeIndex(speechNodeGuid);
npcTalking.ForceSpeechNode(player, conversationData, speechNodeIndex);
}
public static NPCConversationResultAction FindResultAction(NPCTalking npcTalking, string actionString)
{
if (string.IsNullOrEmpty(actionString))
return null;
foreach (var resultAction in npcTalking.conversationResultActions)
{
if (resultAction.action == actionString)
return resultAction;
}
return null;
}
public static SpeechNodeData FindSpeechNodeByGuid(ConversationData conversationData, string speechNodeGuid)
{
foreach (var speechNode in conversationData.speechNodes)
{
if (speechNode is not SpeechNodeData speechNodeData)
continue;
if (speechNodeData.Guid == speechNodeGuid)
return speechNodeData;
}
return null;
}
private static ConversationCondition FindCondition(ResponseNode responseNode, ConversationCondition.ConditionType conditionType, bool? inverse = null)
{
foreach (var condition in responseNode.conditions)
{
if (condition.conditionType == conditionType
&& (inverse == null || condition.inverse == inverse))
return condition;
}
return null;
}
}
#endregion
#region UI
private static class CostLabelUI
{
private const string Name = "VehicleVendorOptions";
public static void Destroy(BasePlayer player)
{
CuiHelper.DestroyUi(player, Name);
}
public static void DestroyAll()
{
foreach (var player in BasePlayer.activePlayerList)
{
Destroy(player);
}
}
public static void Create(VehicleVendorOptions plugin, BasePlayer player, PriceConfig priceConfig)
{
var itemPrice = priceConfig.Amount == 0
? plugin.GetMessage(player, "UI.Price.Free")
: priceConfig.PaymentProvider is EconomicsPaymentProvider
? plugin.GetMessage(player, "UI.Currency.Economics", priceConfig.Amount)
: priceConfig.PaymentProvider is ServerRewardsPaymentProvider
? plugin.GetMessage(player, "UI.Currency.ServerRewards", priceConfig.Amount)
: $"{priceConfig.Amount} {plugin.GetMessage(player, plugin.GetItemNameLocalizationKey(priceConfig.ItemShortName))}";
var cuiElements = new CuiElementContainer
{
{
new CuiLabel
{
RectTransform =
{
AnchorMin = "0.5 0.5",
AnchorMax = "0.5 0.5",
OffsetMin = "152 21",
OffsetMax = "428 41",
},
Text =
{
Text = plugin.GetMessage(player, "UI.ActualPrice", itemPrice),
FontSize = 11,
Font = "robotocondensed-regular.ttf",
Align = UnityEngine.TextAnchor.MiddleLeft,
},
},
"Overlay",
Name
},
};
CuiHelper.AddUi(player, cuiElements);
}
}
#endregion
#region Payment Providers
private interface IPaymentProvider
{
bool IsAvailable { get; }
int GetBalance(BasePlayer player);
void TakeBalance(BasePlayer player, int amount);
}
private class EconomicsPaymentProvider : IPaymentProvider
{
private readonly VehicleVendorOptions _plugin;
private Plugin _ownerPlugin => _plugin.Economics;
public EconomicsPaymentProvider(VehicleVendorOptions plugin)
{
_plugin = plugin;
}
public bool IsAvailable => _ownerPlugin != null;
public int GetBalance(BasePlayer player)
{
return Convert.ToInt32(_ownerPlugin.Call("Balance", (ulong)player.userID));
}
public void TakeBalance(BasePlayer player, int amount)
{
_ownerPlugin.Call("Withdraw", (ulong)player.userID, Convert.ToDouble(amount));
}
}
private class ServerRewardsPaymentProvider : IPaymentProvider
{
private readonly VehicleVendorOptions _plugin;
private Plugin _ownerPlugin => _plugin.ServerRewards;
public ServerRewardsPaymentProvider(VehicleVendorOptions plugin)
{
_plugin = plugin;
}
public bool IsAvailable => _ownerPlugin != null;
public int GetBalance(BasePlayer player)
{
return Convert.ToInt32(_ownerPlugin.Call("CheckPoints", (ulong)player.userID));
}
public void TakeBalance(BasePlayer player, int amount)
{
_ownerPlugin.Call("TakePoints", (ulong)player.userID, amount);
}
}
private class ItemsPaymentProvider : IPaymentProvider
{
public bool IsAvailable => true;
private int _itemId;
public ItemsPaymentProvider(int itemId)
{
_itemId = itemId;
}
public int GetBalance(BasePlayer player)
{
return player.inventory.GetAmount(_itemId);
}
public void TakeBalance(BasePlayer player, int amount)
{
player.inventory.Take(null, _itemId, amount);
}
}
#endregion
#region Configuration
private class VehicleConfigMap
{
[JsonProperty("Minicopter")]
public VehicleConfig Minicopter = new()
{
FuelAmount = 100,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 500 },
new PriceConfig { Amount = 250 },
},
};
[JsonProperty("ScrapTransport")]
public VehicleConfig ScrapTransport = new()
{
FuelAmount = 100,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 800 },
new PriceConfig { Amount = 400 },
},
};
[JsonProperty("AttackHelicopter")]
public VehicleConfig AttackHelicopter = new()
{
FuelAmount = 100,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 1750 },
new PriceConfig { Amount = 1250 },
},
};
[JsonProperty("HotAirBalloon")]
public VehicleConfig HotAirBalloon = new()
{
FuelAmount = 75,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 100 },
new PriceConfig { Amount = 50 },
},
};
[JsonProperty("Rowboat")]
public VehicleConfig Rowboat = new()
{
FuelAmount = 50,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 80 },
new PriceConfig { Amount = 40 },
},
};
[JsonProperty("RHIB")]
public VehicleConfig RHIB = new()
{
FuelAmount = 50,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 200 },
new PriceConfig { Amount = 100 },
},
};
[JsonProperty("SoloSub")]
public VehicleConfig SoloSub = new()
{
FuelAmount = 50,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 125 },
new PriceConfig { Amount = 50 },
},
};
[JsonProperty("DuoSub")]
public VehicleConfig DuoSub = new()
{
FuelAmount = 50,
PricesRequiringPermission = new[]
{
new PriceConfig { Amount = 200 },
new PriceConfig { Amount = 100 },
},
};
}
private class VehicleConfig
{
private static PriceConfig FreePriceConfig = new() { Amount = 0 };
[JsonProperty("RequiresPermission")]
public bool RequiresPermission = false;
[JsonProperty("FuelAmount")]
public int FuelAmount = 100;
[JsonProperty("DespawnProtectionSeconds")]
public float DespawnProtectionSeconds = 300;
[JsonProperty("PricesRequiringPermission")]
public PriceConfig[] PricesRequiringPermission = Array.Empty<PriceConfig>();
public void InitAndValidate(VehicleVendorOptions plugin, string vehicleType)
{
foreach (var priceConfig in PricesRequiringPermission)
{
priceConfig.InitAndValidate(plugin, vehicleType);
plugin.permission.RegisterPermission(priceConfig.Permission, plugin);
}
}
public PriceConfig GetPriceForPlayer(VehicleVendorOptions plugin, IPlayer player, string freePermission)
{
if (plugin.HasPermission(player.Id, Permission_Free_All)
|| plugin.HasPermission(player.Id, freePermission))
return FreePriceConfig;
if (PricesRequiringPermission == null)
return null;
for (var i = PricesRequiringPermission.Length - 1; i >= 0; i--)
{
var priceConfig = PricesRequiringPermission[i];
if (priceConfig.IsValid && player.HasPermission(priceConfig.Permission))
return priceConfig;
}
return null;
}
}
private class PriceConfig
{
[JsonProperty("Amount")]
public int Amount;
[JsonProperty("ItemShortName", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemShortName = "scrap";
[JsonProperty("UseEconomics")]
public bool UseEconomics = false;
[JsonProperty("UseServerRewards")]
public bool UseServerRewards = false;
[JsonIgnore]
public string Permission;
[JsonIgnore]
public IPaymentProvider PaymentProvider;
[JsonIgnore]
public bool IsValid => (PaymentProvider?.IsAvailable ?? false) && Permission != string.Empty;
[JsonIgnore]
public bool RequiresScrap => PaymentProvider is ItemsPaymentProvider
&& ItemShortName == "scrap";
private ItemDefinition _itemDefinition;
[JsonIgnore]
public ItemDefinition ItemDef
{
get
{
if (_itemDefinition == null)
{
_itemDefinition = ItemManager.FindItemDefinition(ItemShortName);
}
return _itemDefinition;
}
}
public bool MatchesVanillaPrice(int vanillaPrice)
{
return RequiresScrap && Amount == vanillaPrice;
}
public void InitAndValidate(VehicleVendorOptions plugin, string vehicleType)
{
Permission = GeneratePermission(vehicleType);
PaymentProvider = CreatePaymentProvider(plugin);
}
private IPaymentProvider CreatePaymentProvider(VehicleVendorOptions plugin)
{
if (UseEconomics)
return new EconomicsPaymentProvider(plugin);
if (UseServerRewards)
return new ServerRewardsPaymentProvider(plugin);
if (ItemDef == null)
{
plugin.LogError($"Price config contains an invalid item short name: '{ItemShortName}'.");
return null;
}
return new ItemsPaymentProvider(ItemDef.itemid);
}
private string GeneratePermission(string vehicleType)
{
if (Amount == 0)
{
Permission = $"{Permission_Price_Prefix}.{vehicleType}.free";
}
else
{
var currencyType = UseEconomics ? "economics"
: UseServerRewards ? "serverrewards"
: ItemShortName;
if (string.IsNullOrEmpty(ItemShortName))
return string.Empty;
Permission = $"{Permission_Price_Prefix}.{vehicleType}.{currencyType}.{Amount}";
}
return Permission;
}
public bool CanPlayerAfford(BasePlayer player)
{
if (Amount <= 0)
return true;
return PaymentProvider.GetBalance(player) >= Amount;
}
public bool TryChargePlayer(BasePlayer player)
{
if (Amount <= 0)
return true;
PaymentProvider.TakeBalance(player, Amount);
return true;
}
}
private class Configuration : BaseConfiguration
{
[JsonProperty("Vehicles")]
public VehicleConfigMap Vehicles = new();
}
private Configuration GetDefaultConfig() => new();
#endregion
#region Configuration Helpers
private class BaseConfiguration
{
public string ToJson() => JsonConvert.SerializeObject(this);
public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
}
private static class JsonHelper
{
public static object Deserialize(string json) => ToObject(JToken.Parse(json));
private static object ToObject(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
return token.Children<JProperty>()
.ToDictionary(prop => prop.Name,
prop => ToObject(prop.Value));
case JTokenType.Array:
return token.Select(ToObject).ToList();
default:
return ((JValue)token).Value;
}
}
}
private bool MaybeUpdateConfig(BaseConfiguration config)
{
var currentWithDefaults = config.ToDictionary();
var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
}
private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
{
var changed = false;
foreach (var key in currentWithDefaults.Keys)
{
if (currentRaw.TryGetValue(key, out var currentRawValue))
{
var currentDictValue = currentRawValue as Dictionary<string, object>;
if (currentWithDefaults[key] is Dictionary<string, object> defaultDictValue)
{
if (currentDictValue == null)
{
currentRaw[key] = currentWithDefaults[key];
changed = true;
}
else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue))
{
changed = true;
}
}
}
else
{
currentRaw[key] = currentWithDefaults[key];
changed = true;
}
}
return changed;
}
protected override void LoadDefaultConfig() => _config = GetDefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null)
{
throw new JsonException();
}
if (MaybeUpdateConfig(_config))
{
LogWarning("Configuration appears to be outdated; updating and saving");
SaveConfig();
}
}
catch (Exception e)
{
LogError(e.Message);
LogWarning($"Configuration file {Name}.json is invalid; using defaults");
LoadDefaultConfig();
}
}
protected override void SaveConfig()
{
Log($"Configuration changes saved to {Name}.json");
Config.WriteObject(_config, true);
}
#endregion
#region Localization
private void ChatMessage(BasePlayer player, string messageName, params object[] args) =>
player.ChatMessage(string.Format(GetMessage(player.UserIDString, messageName), args));
private string GetMessage(BasePlayer player, string messageName, params object[] args) =>
GetMessage(player.IPlayer, messageName, args);
private string GetMessage(IPlayer player, string messageName, params object[] args) =>
GetMessage(player.Id, messageName, args);
private string GetMessage(string playerId, string messageName, params object[] args)
{
var message = lang.GetMessage(messageName, this, playerId);
return args.Length > 0 ? string.Format(message, args) : message;
}
private string GetItemNameLocalizationKey(string itemShortName) => $"Item.{itemShortName}";
private void AddEnglishItemNamesForPriceConfigs(Dictionary<string, string> messages, PriceConfig[] priceConfigs)
{
foreach (var priceConfig in priceConfigs)
{
if (string.IsNullOrEmpty(priceConfig.ItemShortName))
continue;
var localizationKey = GetItemNameLocalizationKey(priceConfig.ItemShortName);
messages[localizationKey] = priceConfig.ItemDef.displayName.english;
}
}
protected override void LoadDefaultMessages()
{
var messages = new Dictionary<string, string>
{
["Error.Vehicle.NoPermission"] = "You don't have permission to buy that vehicle.",
["UI.ActualPrice"] = "Actual price: {0}",
["UI.Price.Free"] = "Free",
["UI.Currency.Economics"] = "{0:C}",
["UI.Currency.ServerRewards"] = "{0} reward points",
};
if (Translate.englishBaseStrings == null)
{
Translate.CacheEnglishStrings();
}
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.Minicopter.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.ScrapTransport.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.AttackHelicopter.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.HotAirBalloon.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.Rowboat.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.RHIB.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.SoloSub.PricesRequiringPermission);
AddEnglishItemNamesForPriceConfigs(messages, _config.Vehicles.DuoSub.PricesRequiringPermission);
lang.RegisterMessages(messages, this, "en");
}
#endregion
}
}