using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using Rust; using Rust.Modular; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using System.Text; namespace Oxide.Plugins { [Info("Spawn Modular Car", "WhiteThunder", "5.3.0")] [Description("Allows players to spawn modular cars.")] internal class SpawnModularCar : CovalencePlugin { #region Fields [PluginReference] private readonly Plugin MonumentFinder, VehicleDeployedLocks; private static SpawnModularCar _pluginInstance; private static Configuration _pluginConfig; private PluginData _pluginData; private CommonPresets _commonPresets; private const string DefaultPresetName = "default"; private const int PresetMaxLength = 30; private const string PermissionSpawnSockets2 = "spawnmodularcar.spawn.2"; private const string PermissionSpawnSockets3 = "spawnmodularcar.spawn.3"; private const string PermissionSpawnSockets4 = "spawnmodularcar.spawn.4"; private const string PermissionEnginePartsTier1 = "spawnmodularcar.engineparts.tier1"; private const string PermissionEnginePartsTier2 = "spawnmodularcar.engineparts.tier2"; private const string PermissionEnginePartsTier3 = "spawnmodularcar.engineparts.tier3"; private const string PermissionFix = "spawnmodularcar.fix"; private const string PermissionFetch = "spawnmodularcar.fetch"; private const string PermissionDespawn = "spawnmodularcar.despawn"; private const string PermissionAutoFuel = "spawnmodularcar.autofuel"; private const string PermissionAutoCodeLock = "spawnmodularcar.autocodelock"; private const string PermissionAutoKeyLock = "spawnmodularcar.autokeylock"; private const string PermissionAutoStartEngine = "spawnmodularcar.autostartengine"; private const string PermissionAutoFillTankers = "spawnmodularcar.autofilltankers"; private const string PermissionGiveCar = "spawnmodularcar.givecar"; private const string PermissionPresets = "spawnmodularcar.presets"; private const string PermissionPresetLoad = "spawnmodularcar.presets.load"; private const string PermissionCommonPresets = "spawnmodularcar.presets.common"; private const string PermissionManageCommonPresets = "spawnmodularcar.presets.common.manage"; private const string PrefabSockets2 = "assets/content/vehicles/modularcar/2module_car_spawned.entity.prefab"; private const string PrefabSockets3 = "assets/content/vehicles/modularcar/3module_car_spawned.entity.prefab"; private const string PrefabSockets4 = "assets/content/vehicles/modularcar/4module_car_spawned.entity.prefab"; private const string ItemDropPrefab = "assets/prefabs/misc/item drop/item_drop.prefab"; private const string RepairEffectPrefab = "assets/bundled/prefabs/fx/build/promote_toptier.prefab"; private const string TankerFilledEffectPrefab = "assets/prefabs/food/water jug/effects/water-jug-fill-container.prefab"; // These layers are used to preventing spawning inside walls or players. private const int BoxcastLayers = Layers.Mask.Default + Layers.Mask.Deployed + Layers.Mask.Player_Server + Layers.Mask.AI + Layers.Mask.Vehicle_Detailed + Layers.Mask.Vehicle_World + Layers.Mask.World + Layers.Mask.Construction + Layers.Mask.Tree; // These layers are used to find a surface to spawn on. private const int RaycastLayers = Layers.Mask.Default + Layers.Mask.Terrain + Layers.World + Layers.Mask.Construction; private static readonly Vector3 ShortCarExtents = new(1, 1.1f, 1.5f); private static readonly Vector3 MediumCarExtents = new(1, 1.1f, 2.3f); private static readonly Vector3 LongCarExtents = new(1, 1.1f, 3); private static readonly Vector3 ShortCarFrontLeft = new(ShortCarExtents.x, 0, ShortCarExtents.z); private static readonly Vector3 ShortCarFrontRight = new(-ShortCarExtents.x, 0, ShortCarExtents.z); private static readonly Vector3 ShortCarBackLeft = new(ShortCarExtents.x, 0, -ShortCarExtents.z); private static readonly Vector3 ShortCarBackRight = new(-ShortCarExtents.x, 0, -ShortCarExtents.z); private static readonly Vector3 MediumCarFrontLeft = new(MediumCarExtents.x, 0, MediumCarExtents.z); private static readonly Vector3 MediumCarFrontRight = new(-MediumCarExtents.x, 0, MediumCarExtents.z); private static readonly Vector3 MediumCarBackLeft = new(MediumCarExtents.x, 0, -MediumCarExtents.z); private static readonly Vector3 MediumCarBackRight = new(-MediumCarExtents.x, 0, -MediumCarExtents.z); private static readonly Vector3 LongCarFrontLeft = new(LongCarExtents.x, 0, LongCarExtents.z); private static readonly Vector3 LongCarFrontRight = new(-LongCarExtents.x, 0, LongCarExtents.z); private static readonly Vector3 LongCarBackLeft = new(LongCarExtents.x, 0, -LongCarExtents.z); private static readonly Vector3 LongCarBackRight = new(-LongCarExtents.x, 0, -LongCarExtents.z); private static readonly float ForwardRaycastDistance = 1.5f + ShortCarExtents.x; private const float DownwardRaycastDistance = 4; private readonly RaycastHit[] _raycastBuffer = new RaycastHit[1]; private readonly Dictionary _playerConfigsMap = new(); #endregion #region Hooks private void Init() { _pluginInstance = this; _pluginData = PluginData.LoadData(); _commonPresets = CommonPresets.LoadData(_pluginData); MigrateConfig(); permission.RegisterPermission(PermissionSpawnSockets2, this); permission.RegisterPermission(PermissionSpawnSockets3, this); permission.RegisterPermission(PermissionSpawnSockets4, this); permission.RegisterPermission(PermissionEnginePartsTier1, this); permission.RegisterPermission(PermissionEnginePartsTier2, this); permission.RegisterPermission(PermissionEnginePartsTier3, this); permission.RegisterPermission(PermissionFix, this); permission.RegisterPermission(PermissionFetch, this); permission.RegisterPermission(PermissionDespawn, this); permission.RegisterPermission(PermissionAutoFuel, this); permission.RegisterPermission(PermissionAutoCodeLock, this); permission.RegisterPermission(PermissionAutoKeyLock, this); permission.RegisterPermission(PermissionAutoStartEngine, this); permission.RegisterPermission(PermissionAutoFillTankers, this); permission.RegisterPermission(PermissionGiveCar, this); permission.RegisterPermission(PermissionPresets, this); permission.RegisterPermission(PermissionPresetLoad, this); permission.RegisterPermission(PermissionCommonPresets, this); permission.RegisterPermission(PermissionManageCommonPresets, this); } private void OnServerInitialized() { if (_pluginConfig.HasMonumentRestriction && MonumentFinder == null) { LogWarning("The Monument Finder plugin is not loaded, so monument restrictions will not work. If you don't want monument restrictions, set \"DisallowedMonuments\": [] in the config to stop seeing this warning."); } } private void Unload() { _pluginInstance = null; _pluginConfig = null; } private void OnNewSave(string filename) { _pluginData.PlayerCars.Clear(); _pluginData.Cooldowns.ClearAll(); _pluginData.SaveData(); } private void OnEntityKill(ModularCar car) { if (!IsPlayerCar(car)) return; var userId = _pluginData.PlayerCars.FirstOrDefault(x => x.Value == car.net.ID.Value).Key; var player = BasePlayer.Find(userId); if (player != null) ChatMessage(player, "Generic.Info.CarDestroyed"); _pluginData.UnregisterCar(userId); } private void OnEngineStarted(ModularCar car, BasePlayer player) { if (car == null || car.OwnerID == 0 || !_pluginData.PlayerCars.ContainsValue(car.net.ID.Value) || !permission.UserHasPermission(car.OwnerID.ToString(), PermissionAutoStartEngine)) return; if (car.engineController.IsStarting) { car.CancelInvoke(car.engineController.FinishStartingEngine); car.engineController.FinishStartingEngine(); } } #endregion #region API private static class ApiParser { public static string CodeLockField = "CodeLock"; public static string KeyLockField = "KeyLock"; public static string EnginePartsTierField = "EnginePartsTier"; public static string FreshWaterAmountField = "FreshWaterAmount"; public static string FuelAmountField = "FuelAmount"; public static string ModulesField = "Modules"; public static bool TryParseOptions(Dictionary options, out PresetCarOptions presetOptions) { var codeLock = BoolOption(options, CodeLockField); var keyLock = BoolOption(options, KeyLockField); var enginePartsTier = IntOption(options, EnginePartsTierField); var freshWaterAmount = IntOption(options, FreshWaterAmountField); var fuelAmount = IntOption(options, FuelAmountField); var moduleIDs = ParseModulesOption(options); presetOptions = null; if (moduleIDs == null) { _pluginInstance.LogError($"[API] '{ApiParser.ModulesField}' field is missing or unrecognizable."); return false; } if (moduleIDs.Length is < 2 or > 4) { _pluginInstance.LogError($"[API] Requested a car with {moduleIDs.Length} sockets, but only 2-4 sockets is supported."); return false; } presetOptions = new PresetCarOptions { CodeLock = codeLock, KeyLock = keyLock, EnginePartsTier = enginePartsTier, FreshWaterAmount = freshWaterAmount, FuelAmount = fuelAmount, NormalizedModuleIDs = moduleIDs }; return true; } private static bool BoolOption(Dictionary options, string name) { return options.TryGetValue(name, out var value) && value is true; } private static int IntOption(Dictionary options, string name) { return options.TryGetValue(name, out var value) && value is int i ? i : 0; } public static int[] ParseModulesOption(Dictionary options) { if (!options.ContainsKey(ModulesField)) return null; if (options[ModulesField] is not object[] moduleArray) return null; return _pluginInstance.ValidateModules(moduleArray); } } private ModularCar API_SpawnPreset(Dictionary options, BasePlayer player, Vector3 position, Quaternion rotation) { if (!ApiParser.TryParseOptions(options, out var presetOptions)) return null; if (SpawnWasBlocked(player)) return null; if (position == Vector3.zero && player != null) DetermineCarPositionAndRotation(player, presetOptions.Length, out position, out rotation); return SpawnCar(presetOptions, position, rotation, player, shouldTrackCar: false); } private ModularCar API_SpawnNamedPreset(string presetName, BasePlayer player, Vector3 position, Quaternion rotation) { var presetOptions = _pluginConfig.FindPreset(presetName)?.Options; if (presetOptions == null) { LogError($"[API] Server preset '{presetName}' not found."); return null; } if (presetOptions.Length is < 2 or > 4) { LogError($"[API] Requested a car with {presetOptions.Length} sockets, but only 2-4 sockets is supported."); return null; } if (SpawnWasBlocked(player)) return null; if (position == Vector3.zero && player != null) DetermineCarPositionAndRotation(player, presetOptions.Length, out position, out rotation); return SpawnCar(presetOptions, position, rotation, player, shouldTrackCar: false); } private ModularCar API_SpawnPresetCar(BasePlayer player, Dictionary options, Action onReady = null) { if (!ApiParser.TryParseOptions(options, out var presetOptions)) return null; if (SpawnWasBlocked(player)) return null; if (!TryGetIdealCarPositionAndRotation(player, presetOptions.Length, out var spawnPosition, out var rotation)) { spawnPosition = GetFixedCarPosition(player); rotation = GetRelativeCarRotation(player); } var car = SpawnCar(presetOptions, spawnPosition, rotation, player, shouldTrackCar: false); if (car != null) { // Note: Consumers no longer need to use this callback since this plugin now forces synchronous module registration. onReady?.Invoke(car); } return car; } #endregion #region Commands [Command("givecar")] private void SpawnCarServerCommand(IPlayer player, string cmd, string[] args) { if (!player.IsServer && !VerifyPermissionAny(player, PermissionGiveCar)) return; if (args.Length < 2) { ReplyToPlayer(player, "Command.Give.Error.Syntax"); return; } var playerNameOrIdArg = args[0]; var presetNameArg = args[1]; var targetPlayer = BasePlayer.Find(playerNameOrIdArg); if (targetPlayer == null) { ReplyToPlayer(player, "Command.Give.Error.PlayerNotFound", playerNameOrIdArg); return; } var preset = _pluginConfig.FindPreset(presetNameArg); if (preset == null) { ReplyToPlayer(player, "Generic.Error.PresetNotFound", presetNameArg); return; } var carOptions = preset.Options; if (carOptions.Length < 2) { ReplyToPlayer(player, "Command.Give.Error.PresetTooFewModules", preset.Name, carOptions.Length); return; } if (carOptions.Length > 4) { ReplyToPlayer(player, "Command.Give.Error.PresetTooManyModules", preset.Name, carOptions.Length); return; } if (!TryGetIdealCarPositionAndRotation(targetPlayer, preset.Options.Length, out var spawnPosition, out var rotation)) { spawnPosition = GetFixedCarPosition(targetPlayer); rotation = GetRelativeCarRotation(targetPlayer); } var car = SpawnCar(carOptions, spawnPosition, rotation, targetPlayer, shouldTrackCar: false); if (car != null) { ReplyToPlayer(player, "Command.Give.Success", targetPlayer.displayName, preset.Name); } } [Command("mycar")] private void MyCarCommand(IPlayer player, string cmd, string[] args) { if (player.IsServer) return; var basePlayer = player.Object as BasePlayer; if (!basePlayer.CanInteract()) return; if (args.Length == 0) { SubCommand_SpawnCar(player, args); return; } switch (args[0].ToLower()) { case "help": SubCommand_Help(player, args.Skip(1).ToArray()); return; case "list": SubCommand_ListPresets(player, args.Skip(1).ToArray()); return; case "save": SubCommand_SavePreset(player, args.Skip(1).ToArray()); return; case "update": SubCommand_UpdatePreset(player, args.Skip(1).ToArray()); return; case "load": SubCommand_LoadPreset(player, args.Skip(1).ToArray()); return; case "rename": SubCommand_RenamePreset(player, args.Skip(1).ToArray()); return; case "delete": SubCommand_DeletePreset(player, args.Skip(1).ToArray()); return; case "fix": SubCommand_FixCar(player, args.Skip(1).ToArray()); return; case "fetch": SubCommand_FetchCar(player, args.Skip(1).ToArray()); return; case "destroy": SubCommand_DestroyCar(player, args.Skip(1).ToArray()); return; case "autocodelock": SubCommand_ToggleAutoCodeLock(player, args.Skip(1).ToArray()); return; case "autokeylock": SubCommand_ToggleAutoKeyLock(player, args.Skip(1).ToArray()); return; case "autofilltankers": SubCommand_ToggleAutoFillTankers(player, args.Skip(1).ToArray()); return; case "common": SubCommand_CommonPreset(player, args.Skip(1).ToArray()); return; default: SubCommand_SpawnCar(player, args); return; } } private void SubCommand_CommonPreset(IPlayer player, string[] args) { if (args.Length == 0) { ReplyToPlayer(player, "Command.Common.Error.Syntax"); return; } switch (args[0].ToLower()) { case "list": SubCommand_Common_ListPresets(player, args.Skip(1).ToArray()); return; case "load": SubCommand_Common_LoadPreset(player, args.Skip(1).ToArray()); return; case "save": SubCommand_Common_SavePreset(player, args.Skip(1).ToArray()); return; case "update": SubCommand_Common_UpdatePreset(player, args.Skip(1).ToArray()); return; case "rename": SubCommand_Common_RenamePreset(player, args.Skip(1).ToArray()); return; case "delete": SubCommand_Common_DeletePreset(player, args.Skip(1).ToArray()); return; default: SubCommand_Common_SpawnCar(player, args); return; } } private void SubCommand_Help(IPlayer player, string[] args) { var maxAllowedSockets = GetPlayerMaxAllowedCarSockets(player.Id); if (maxAllowedSockets == 0) { ReplyToPlayer(player, "Generic.Error.NoPermission"); return; } var canUsePresets = permission.UserHasPermission(player.Id, PermissionPresets); var canLoadPresets = permission.UserHasPermission(player.Id, PermissionPresetLoad); var sb = new StringBuilder(); sb.AppendLine(GetMessage(player, "Command.Help")); if (canUsePresets) { sb.AppendLine(GetMessage(player, "Command.Help.Spawn.Basic.PresetsAllowed")); } else { sb.AppendLine(GetMessage(player, "Command.Help.Spawn.Basic")); } sb.AppendLine(GetMessage(player, "Command.Help.Spawn.Sockets")); if (permission.UserHasPermission(player.Id, PermissionFix)) { sb.AppendLine(GetMessage(player, "Command.Help.Fix")); } if (permission.UserHasPermission(player.Id, PermissionFetch)) { sb.AppendLine(GetMessage(player, "Command.Help.Fetch")); } if (permission.UserHasPermission(player.Id, PermissionDespawn)) { sb.AppendLine(GetMessage(player, "Command.Help.Destroy")); } if (canUsePresets) { sb.AppendLine(GetMessage(player, "Command.Help.Section.PersonalPresets")); sb.AppendLine(GetMessage(player, "Command.Help.ListPresets")); sb.AppendLine(GetMessage(player, "Command.Help.Spawn.Preset")); if (canLoadPresets) { sb.AppendLine(GetMessage(player, "Command.Help.LoadPreset")); } sb.AppendLine(GetMessage(player, "Command.Help.SavePreset")); sb.AppendLine(GetMessage(player, "Command.Help.UpdatePreset")); sb.AppendLine(GetMessage(player, "Command.Help.RenamePreset")); sb.AppendLine(GetMessage(player, "Command.Help.DeletePreset")); } if (permission.UserHasPermission(player.Id, PermissionCommonPresets)) { sb.AppendLine(GetMessage(player, "Command.Help.Section.CommonPresets")); sb.AppendLine(GetMessage(player, "Command.Help.Common.ListPresets")); sb.AppendLine(GetMessage(player, "Command.Help.Common.Spawn")); if (canLoadPresets) { sb.AppendLine(GetMessage(player, "Command.Help.Common.LoadPreset")); } if (permission.UserHasPermission(player.Id, PermissionManageCommonPresets)) { sb.AppendLine(GetMessage(player, "Command.Help.Common.SavePreset")); sb.AppendLine(GetMessage(player, "Command.Help.Common.UpdatePreset")); sb.AppendLine(GetMessage(player, "Command.Help.Common.RenamePreset")); sb.AppendLine(GetMessage(player, "Command.Help.Common.DeletePreset")); } } var canCodeLock = VehicleDeployedLocks != null && permission.UserHasPermission(player.Id, PermissionAutoCodeLock); var canKeyLock = permission.UserHasPermission(player.Id, PermissionAutoKeyLock); var canFillTankers = permission.UserHasPermission(player.Id, PermissionAutoFillTankers); if (canCodeLock || canKeyLock || canFillTankers) { sb.AppendLine(GetMessage(player, "Command.Help.Section.PersonalSettings")); } if (canCodeLock) { sb.AppendLine(GetMessage(player, "Command.Help.ToggleAutoCodeLock", BooleanToLocalizedString(player, GetPlayerConfig(player).Settings.AutoCodeLock))); } if (canKeyLock) { sb.AppendLine(GetMessage(player, "Command.Help.ToggleAutoKeyLock", BooleanToLocalizedString(player, GetPlayerConfig(player).Settings.AutoKeyLock))); } if (canFillTankers) { sb.AppendLine(GetMessage(player, "Command.Help.ToggleAutoFillTankers", BooleanToLocalizedString(player, GetPlayerConfig(player).Settings.AutoFillTankers))); } if (permission.UserHasPermission(player.Id, PermissionGiveCar)) { sb.AppendLine("Command.Help.Section.OtherCommands"); sb.AppendLine(GetMessage(player, "Command.Help.Give")); } player.Reply(sb.ToString()); } private void SubCommand_SpawnCar(IPlayer player, string[] args) { var maxAllowedSockets = GetPlayerMaxAllowedCarSockets(player.Id); if (maxAllowedSockets == 0) { ReplyToPlayer(player, "Generic.Error.NoPermission"); return; } if (!VerifyHasNoCar(player) || !VerifyOffCooldown(player, CooldownType.Spawn) || !VerifyLocationNotRestricted(player) || !_pluginConfig.CanSpawnBuildingBlocked && !VerifyNotBuildingBlocked(player)) return; // Key binds automatically pass the "True" argument. var wasPassedArgument = args.Length > 0 && args[0] != "True"; if (wasPassedArgument) { if (int.TryParse(args[0], out var desiredSockets)) { if (desiredSockets is < 2 or > 4) { ReplyToPlayer(player, "Command.Spawn.Error.SocketSyntax"); return; } if (desiredSockets > maxAllowedSockets) { ReplyToPlayer(player, "Generic.Error.NoPermission"); return; } SpawnRandomCarForPlayer(player, desiredSockets); return; } if (!VerifyPermissionAny(player, PermissionPresets)) return; var presetNameArg = args[0]; if (!VerifyOnlyOneMatchingPreset(player, GetPlayerConfig(player), presetNameArg, out var preset)) return; SpawnPresetCarForPlayer(player, preset); } else { if (permission.UserHasPermission(player.Id, PermissionPresets)) { var preset = GetPlayerConfig(player).FindPreset(DefaultPresetName); if (preset != null) { SpawnPresetCarForPlayer(player, preset); return; } } SpawnRandomCarForPlayer(player, maxAllowedSockets); } } private void SubCommand_Common_SpawnCar(IPlayer player, string[] args) { var maxAllowedSockets = GetPlayerMaxAllowedCarSockets(player.Id); if (maxAllowedSockets == 0) { ReplyToPlayer(player, "Generic.Error.NoPermission"); return; } if (!VerifyPermissionAny(player, PermissionCommonPresets) || !VerifyHasNoCar(player) || !VerifyOffCooldown(player, CooldownType.Spawn) || !VerifyLocationNotRestricted(player) || !_pluginConfig.CanSpawnBuildingBlocked && !VerifyNotBuildingBlocked(player)) return; var presetNameArg = args[0]; if (!VerifyOnlyOneMatchingPreset(player, _commonPresets, presetNameArg, out var preset)) return; SpawnPresetCarForPlayer(player, preset); } private void SubCommand_FixCar(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionFix)) return; if (!VerifyHasCar(player, out var car) || !VerifyOffCooldown(player, CooldownType.Fix) || FixMyCarWasBlocked(player.Object as BasePlayer, car)) return; if (car.IsDead()) ReviveCar(car); FixCar(car, GetPlayerAllowedFuel(player.Id), GetPlayerEnginePartsTier(player.Id)); MaybeFillTankerModules(car, GetPlayerAllowedFreshWater(player.Id)); _pluginData.StartCooldown(player.Id, CooldownType.Fix); MaybePlayCarRepairEffects(car); ReplyToPlayer(player, "Command.Fix.Success"); } private void SubCommand_FetchCar(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionFetch)) return; var basePlayer = player.Object as BasePlayer; if (!VerifyHasCar(player, out var car) || !_pluginConfig.CanFetchOccupied && !VerifyCarNotOccupied(player, car) || !VerifyOffCooldown(player, CooldownType.Fetch) || !VerifyLocationNotRestricted(player) || !_pluginConfig.CanFetchBuildingBlocked && !VerifyNotBuildingBlocked(player) || !VerifySufficientSpace(player, car.TotalSockets, out var fetchPosition, out var fetchRotation) || FetchMyCarWasBlocked(basePlayer, car)) return; // This is a hacky way to determine that the car is on a lift. if (car.rigidBody.isKinematic && !TryReleaseCarFromLift(car)) { var messages = new List { GetMessage(player, "Command.Fetch.Error.StuckOnLift") }; if (permission.UserHasPermission(player.Id, PermissionDespawn)) messages.Add(GetMessage(player, "Command.Fetch.Error.StuckOnLift.Help")); player.Reply(string.Join(" ", messages)); return; } if (_pluginConfig.DismountPlayersOnFetch) { DismountAllPlayersFromCar(car); } // Temporarily clear max angular velocity to prevent the car from unexpectedly spinning when teleporting really far. var maxAngularVelocity = car.rigidBody.maxAngularVelocity; car.rigidBody.maxAngularVelocity = 0; car.transform.SetPositionAndRotation(fetchPosition, fetchRotation); car.SetVelocity(Vector3.zero); car.SetAngularVelocity(Vector3.zero); car.UpdateNetworkGroup(); car.SendNetworkUpdateImmediate(); timer.Once(1f, () => { if (car != null) { car.rigidBody.maxAngularVelocity = maxAngularVelocity; } }); _pluginData.StartCooldown(player.Id, CooldownType.Fetch); ReplyToPlayer(player, "Command.Fetch.Success"); } private void SubCommand_DestroyCar(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionDespawn)) return; var basePlayer = player.Object as BasePlayer; if (!VerifyHasCar(player, out var car) || !_pluginConfig.CanDespawnOccupied && !VerifyCarNotOccupied(player, car) || DestroyMyCarWasBlocked(basePlayer, car)) return; var extractedEngineParts = ExtractEnginePartsAboveTierAndDeleteRest(car, GetPlayerEnginePartsTier(player.Id)); car.Kill(); if (extractedEngineParts.Count > 0) { GiveItemsToPlayerOrDrop(basePlayer, extractedEngineParts); ReplyToPlayer(player, "Generic.Info.PartsRecovered"); } } private void SubCommand_ListPresets(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresets)) return; var config = GetPlayerConfig(player); if (config.Presets.Count == 0) { ReplyToPlayer(player, "Generic.Error.NoPresets"); return; } var presetList = config.Presets.Select(p => p).ToList(); presetList.Sort(SortPresetNames); var sb = new StringBuilder(); sb.AppendLine(GetMessage(player, "Command.List")); foreach (var preset in presetList) { sb.AppendLine(GetMessage(player, "Command.List.Item", preset.Name, preset.NumSockets)); } player.Reply(sb.ToString()); } private void SubCommand_Common_ListPresets(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionCommonPresets)) return; if (_commonPresets.Presets.Count == 0) { ReplyToPlayer(player, "Generic.Error.NoCommonPresets"); return; } var maxAllowedSockets = GetPlayerMaxAllowedCarSockets(player.Id); var presetList = _commonPresets.Presets.Where(p => p.NumSockets <= maxAllowedSockets).ToList(); presetList.Sort(SortPresetNames); var sb = new StringBuilder(); sb.AppendLine(GetMessage(player, "Command.Common.List")); foreach (var preset in presetList) { sb.AppendLine(GetMessage(player, "Command.List.Item", preset.Name, preset.NumSockets)); } player.Reply(sb.ToString()); } private void SubCommand_SavePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresets)) return; if (!VerifyHasCar(player, out var car)) return; var presetNameArg = args.Length == 0 ? DefaultPresetName : args[0]; var presetManager = GetPlayerConfig(player); if (!VerifyNoMatchingPreset(player, presetManager, presetNameArg)) return; if (presetManager.Presets.Count >= _pluginConfig.MaxPresetsPerPlayer) { ReplyToPlayer(player, "Command.SavePreset.Error.TooManyPresets", _pluginConfig.MaxPresetsPerPlayer); return; } SavePreset(player, presetManager, presetNameArg, car); } private void SubCommand_Common_SavePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionManageCommonPresets)) return; if (args.Length == 0) { ReplyToPlayer(player, "Command.Common.SavePreset.Error.Syntax"); return; } var presetNameArg = args[0]; if (!VerifyHasCar(player, out var car) || !VerifyNoMatchingPreset(player, _commonPresets, presetNameArg)) return; SavePreset(player, _commonPresets, presetNameArg, car); } private void SavePreset(IPlayer player, SimplePresetManager presetManager, string presetNameArg, ModularCar car) { if (presetNameArg.Length > PresetMaxLength) { ReplyToPlayer(player, "Generic.Error.PresetNameLength", PresetMaxLength); return; } presetManager.SavePreset(SimplePreset.FromCar(car, presetNameArg)); ReplyToPlayer(player, "Command.SavePreset.Success", presetNameArg); } private void SubCommand_UpdatePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresets)) return; var presetNameArg = args.Length == 0 ? DefaultPresetName : args[0]; UpdatePreset(player, GetPlayerConfig(player), presetNameArg); } private void SubCommand_Common_UpdatePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionManageCommonPresets)) return; if (args.Length == 0) { ReplyToPlayer(player, "Command.Common.UpdatePreset.Error.Syntax"); return; } UpdatePreset(player, _commonPresets, args[0]); } private void UpdatePreset(IPlayer player, SimplePresetManager presetManager, string presetNameArg) { if (!VerifyHasCar(player, out var car)) return; if (!VerifyHasPreset(player, presetManager, presetNameArg, out var preset)) return; presetManager.UpdatePreset(SimplePreset.FromCar(car, preset.Name)); ReplyToPlayer(player, "Command.UpdatePreset.Success", preset.Name); } private void SubCommand_LoadPreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresetLoad)) return; var presetNameArg = args.Length == 0 ? DefaultPresetName : args[0]; LoadPreset(player, GetPlayerConfig(player.Id), presetNameArg); } private void SubCommand_Common_LoadPreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresetLoad) || !VerifyPermissionAny(player, PermissionCommonPresets)) return; if (args.Length == 0) { ReplyToPlayer(player, "Command.Common.LoadPreset.Error.Syntax"); return; } var presetNameArg = args[0]; LoadPreset(player, _commonPresets, presetNameArg); } private void LoadPreset(IPlayer player, SimplePresetManager presetManager, string presetNameArg) { var basePlayer = player.Object as BasePlayer; if (!VerifyHasCar(player, out var car) || !VerifyCarNotOccupied(player, car) || !VerifyOffCooldown(player, CooldownType.Load) || LoadMyCarPresetWasBlocked(basePlayer, car)) return; if (!VerifyOnlyOneMatchingPreset(player, presetManager, presetNameArg, out var preset)) return; var presetNumSockets = preset.NumSockets; if (presetNumSockets > GetPlayerMaxAllowedCarSockets(player.Id)) { ReplyToPlayer(player, "Generic.Error.NoPermissionToPresetSocketCount", preset.Name, preset.NumSockets); return; } if (presetNumSockets != car.TotalSockets) { ReplyToPlayer(player, "Command.LoadPreset.Error.SocketCount", preset.Name, presetNumSockets, car.TotalSockets); return; } if (car.IsDead()) { ReviveCar(car); } var wasEngineOn = car.IsOn(); var enginePartsTier = GetPlayerEnginePartsTier(player.Id); var extractedEngineParts = ExtractEnginePartsAboveTierAndDeleteRest(car, enginePartsTier); UpdateCarModules(car, preset.ModuleIDs); _pluginData.StartCooldown(player.Id, CooldownType.Load); NextTick(() => { var wereExtraParts = false; if (extractedEngineParts.Count > 0) { var remainingEngineParts = AddEngineItemsAndReturnRemaining(car, extractedEngineParts); if (remainingEngineParts.Count > 0) { wereExtraParts = true; GiveItemsToPlayerOrDrop(basePlayer, remainingEngineParts); } } FixCar(car, GetPlayerAllowedFuel(player.Id), enginePartsTier); // Restart the engine if it turned off during the brief moment it had no engine or no parts. if (wasEngineOn && !car.IsOn() && car.engineController.CanRunEngine()) car.engineController.FinishStartingEngine(); MaybeFillTankerModules(car, GetPlayerAllowedFreshWater(player.Id)); if (car.CarLock.HasALock && !car.CarLock.CanHaveALock()) { car.RemoveLock(); } MaybePlayCarRepairEffects(car); var chatMessages = new List() { GetMessage(player, "Command.LoadPreset.Success", preset.Name) }; if (wereExtraParts) chatMessages.Add(GetMessage(player, "Generic.Info.PartsRecovered")); player.Reply(string.Join(" ", chatMessages)); }); } private void SubCommand_RenamePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresets)) return; if (args.Length < 2) { ReplyToPlayer(player, "Command.RenamePreset.Error.Syntax"); return; } RenamePreset(player, GetPlayerConfig(player), args[0], args[1]); } private void SubCommand_Common_RenamePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionManageCommonPresets)) return; if (args.Length < 2) { ReplyToPlayer(player, "Command.Common.RenamePreset.Error.Syntax"); return; } RenamePreset(player, _commonPresets, args[0], args[1]); } private void RenamePreset(IPlayer player, SimplePresetManager presetManager, string oldName, string newName) { if (!VerifyHasPreset(player, presetManager, oldName, out var preset)) return; // Cache actual old preset name since matching is case-insensitive. var actualOldPresetName = preset.Name; var existingPresetWithNewName = presetManager.FindPreset(newName); if (newName.Length > PresetMaxLength) { ReplyToPlayer(player, "Generic.Error.PresetNameLength", PresetMaxLength); return; } // Allow renaming if just changing case. if (existingPresetWithNewName != null && preset != existingPresetWithNewName) { ReplyToPlayer(player, "Generic.Error.PresetAlreadyTaken", existingPresetWithNewName.Name); return; } presetManager.RenamePreset(preset, newName); ReplyToPlayer(player, "Command.RenamePreset.Success", actualOldPresetName, newName); } private void SubCommand_DeletePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionPresets)) return; var presetNameArg = args.Length == 0 ? DefaultPresetName : args[0]; DeletePreset(player, GetPlayerConfig(player), presetNameArg); } private void SubCommand_Common_DeletePreset(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionManageCommonPresets)) return; if (args.Length == 0) { ReplyToPlayer(player, "Command.Common.DeletePreset.Error.Syntax"); return; } DeletePreset(player, _commonPresets, args[0]); } private void DeletePreset(IPlayer player, SimplePresetManager presetManager, string presetNameArg) { if (!VerifyHasPreset(player, presetManager, presetNameArg, out var preset)) return; presetManager.DeletePreset(preset); ReplyToPlayer(player, "Command.DeletePreset.Success", preset.Name); } private void SubCommand_ToggleAutoCodeLock(IPlayer player, string[] args) { if (VehicleDeployedLocks == null || !VerifyPermissionAny(player, PermissionAutoCodeLock)) return; var config = GetPlayerConfig(player); config.Settings.AutoCodeLock = !config.Settings.AutoCodeLock; config.SaveData(); ReplyToPlayer(player, "Command.ToggleAutoCodeLock.Success", BooleanToLocalizedString(player, config.Settings.AutoCodeLock)); } private void SubCommand_ToggleAutoKeyLock(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionAutoKeyLock)) return; var config = GetPlayerConfig(player); config.Settings.AutoKeyLock = !config.Settings.AutoKeyLock; config.SaveData(); ReplyToPlayer(player, "Command.ToggleAutoKeyLock.Success", BooleanToLocalizedString(player, config.Settings.AutoKeyLock)); } private void SubCommand_ToggleAutoFillTankers(IPlayer player, string[] args) { if (!VerifyPermissionAny(player, PermissionAutoFillTankers)) return; var config = GetPlayerConfig(player); config.Settings.AutoFillTankers = !config.Settings.AutoFillTankers; config.SaveData(); ReplyToPlayer(player, "Command.ToggleAutoFillTankers.Success", BooleanToLocalizedString(player, config.Settings.AutoFillTankers)); } #endregion #region Dependencies private class MonumentAdapter { public string ShortName => (string)_monumentInfo["ShortName"]; private Dictionary _monumentInfo; public MonumentAdapter(Dictionary monumentInfo) { _monumentInfo = monumentInfo; } public bool IsInBounds(Vector3 position) => ((Func)_monumentInfo["IsInBounds"]).Invoke(position); } private MonumentAdapter GetClosestMonument(Vector3 position) { return MonumentFinder?.Call("API_GetClosest", position) is Dictionary dictResult ? new MonumentAdapter(dictResult) : null; } #endregion #region Helper Methods - Command Checks private static bool SpawnWasBlocked(BasePlayer player) { var hookResult = Interface.CallHook("CanSpawnModularCar", player); return hookResult is false; } private static bool SpawnMyCarWasBlocked(BasePlayer player) { if (SpawnWasBlocked(player)) return true; var hookResult = Interface.CallHook("CanSpawnMyCar", player); return hookResult is false; } private static bool FetchMyCarWasBlocked(BasePlayer player, ModularCar car) { return Interface.CallHook("CanFetchMyCar", player, car) is false; } private static bool FixMyCarWasBlocked(BasePlayer player, ModularCar car) { return Interface.CallHook("CanFixMyCar", player, car) is false; } private static bool LoadMyCarPresetWasBlocked(BasePlayer player, ModularCar car) { return Interface.CallHook("CanLoadMyCarPreset", player, car) is false; } private static bool DestroyMyCarWasBlocked(BasePlayer player, ModularCar car) { return Interface.CallHook("CanDestroyMyCar", player, car) is false; } private static bool HasParent(BasePlayer player) where T : BaseEntity { var parent = player.GetParentEntity(); while (parent != null) { if (parent is T) return true; parent = parent.GetParentEntity(); } return false; } private bool IsMonumentAllowed(BasePlayer basePlayer) { if (!_pluginConfig.HasMonumentRestriction || MonumentFinder == null) return true; var position = basePlayer.transform.position; return GetClosestMonument(position) is not {} monument || !monument.IsInBounds(position) || _pluginConfig.IsMonumentAllowed(monument.ShortName); } private bool IsOnCargoShip(BasePlayer basePlayer) { return HasParent(basePlayer); } private bool VerifyPermissionAny(IPlayer player, params string[] permissionNames) { foreach (var perm in permissionNames) { if (!permission.UserHasPermission(player.Id, perm)) { ReplyToPlayer(player, "Generic.Error.NoPermission"); return false; } } return true; } private bool VerifyLocationNotRestricted(IPlayer player) { var basePlayer = player.Object as BasePlayer; if (IsOnCargoShip(basePlayer) || !IsMonumentAllowed(basePlayer)) { ReplyToPlayer(player, "Generic.Error.LocationRestricted"); return false; } return true; } private bool VerifyNotBuildingBlocked(IPlayer player) { if ((player.Object as BasePlayer).IsBuildingBlocked()) { ReplyToPlayer(player, "Generic.Error.BuildingBlocked"); return false; } return true; } private bool VerifySufficientSpace(IPlayer player, int numSockets, out Vector3 determinedPosition, out Quaternion determinedRotation) { var basePlayer = player.Object as BasePlayer; if (!TryGetIdealCarPositionAndRotation(basePlayer, numSockets, out determinedPosition, out determinedRotation) || !HasSufficientSpace(basePlayer, numSockets, determinedPosition, determinedRotation)) { ReplyToPlayer(player, "Generic.Error.InsufficientSpace"); return false; } return true; } private bool VerifyHasPreset(IPlayer player, SimplePresetManager presetManager, string presetName, out SimplePreset preset) { preset = presetManager.FindPreset(presetName); if (preset == null) { ReplyToPlayer(player, "Generic.Error.PresetNotFound", presetName); return false; } return true; } private bool VerifyNoMatchingPreset(IPlayer player, SimplePresetManager presetManager, string presetName) { var existingPreset = presetManager.FindPreset(presetName); if (existingPreset != null) { ReplyToPlayer(player, "Command.SavePreset.Error.PresetAlreadyExists", existingPreset.Name); return false; } return true; } private bool VerifyHasCar(IPlayer player, out ModularCar car) { car = FindPlayerCar(player); if (car == null) { ReplyToPlayer(player, "Generic.Error.CarNotFound"); return false; } return true; } private bool VerifyHasNoCar(IPlayer player) { if (FindPlayerCar(player) == null) return true; var messages = new List { GetMessage(player, "Command.Spawn.Error.CarAlreadyExists") }; if (permission.UserHasPermission(player.Id, PermissionFetch)) messages.Add(GetMessage(player, "Command.Spawn.Error.CarAlreadyExists.Help")); player.Reply(string.Join(" ", messages)); return false; } private bool VerifyCarNotOccupied(IPlayer player, ModularCar car) { // Players can either be mounted in seats, or standing on flatbed modules. if (car.AnyMounted() || car.AttachedModuleEntities.Any(module => module.children.Any(child => child is BasePlayer))) { ReplyToPlayer(player, "Generic.Error.CarOccupied"); return false; } return true; } private bool VerifyOffCooldown(IPlayer player, CooldownType cooldownType) { var secondsRemaining = _pluginData.GetRemainingCooldownSeconds(player.Id, cooldownType); if (secondsRemaining > 0) { ReplyToPlayer(player, "Generic.Error.Cooldown", secondsRemaining); return false; } return true; } private bool VerifyOnlyOneMatchingPreset(IPlayer player, SimplePresetManager presetManager, string presetName, out SimplePreset preset) { preset = presetManager.FindPreset(presetName); if (preset != null) return true; var matchingPresets = presetManager.FindMatchingPresets(presetName); var matchCount = matchingPresets.Count; if (matchCount == 0) { ReplyToPlayer(player, "Generic.Error.PresetNotFound", presetName); return false; } if (matchCount > 1) { ReplyToPlayer(player, "Generic.Error.PresetMultipleMatches", presetName); return false; } preset = matchingPresets.First(); return true; } #endregion #region Helper Methods - Cars private static int SortPresetNames(SimplePreset a, SimplePreset b) => a.Name.ToLower() == DefaultPresetName ? -1 : b.Name.ToLower() == DefaultPresetName ? 1 : a.Name.CompareTo(b.Name); private static Vector3 GetCarExtents(int numSockets) { switch (numSockets) { case 2: return ShortCarExtents; case 3: return MediumCarExtents; default: return LongCarExtents; } } private static void GetCarFrontBack(int numSockets, out Vector3 frontLeft, out Vector3 frontRight, out Vector3 backLeft, out Vector3 backRight) { switch (numSockets) { case 2: frontLeft = ShortCarFrontLeft; frontRight = ShortCarFrontRight; backLeft = ShortCarBackLeft; backRight = ShortCarBackRight; return; case 3: frontLeft = MediumCarFrontLeft; frontRight = MediumCarFrontRight; backLeft = MediumCarBackLeft; backRight = MediumCarBackRight; return; default: frontLeft = LongCarFrontLeft; frontRight = LongCarFrontRight; backLeft = LongCarBackLeft; backRight = LongCarBackRight; return; } } private static int[] GetCarModuleIDs(ModularCar car) { var moduleIDs = new List(); for (var socketIndex = 0; socketIndex < car.TotalSockets; socketIndex++) { if (car.TryGetModuleAt(socketIndex, out var module) && module.FirstSocketIndex == socketIndex) moduleIDs.Add(module.AssociatedItemDef.itemid); else // Use 0 to represent an empty socket. moduleIDs.Add(0); } return moduleIDs.ToArray(); } private static Vector3 GetPlayerForwardPosition(BasePlayer player) { var forward = player.GetNetworkRotation() * Vector3.forward; forward.y = 0; return forward.normalized; } // Directly in front of the player. private static Vector3 GetFixedCarPosition(BasePlayer player) { var forward = GetPlayerForwardPosition(player); var position = player.transform.position + forward * 3f; position.y = player.transform.position.y + 1f; return position; } // On surface in front of player. private static bool TryGetIdealCarPositionAndRotation(BasePlayer player, int numSockets, out Vector3 position, out Quaternion rotation) { var carMiddle = player.eyes.position + GetPlayerForwardPosition(player) * ForwardRaycastDistance; GetCarFrontBack(numSockets, out var carFrontLeft, out var carFrontRight, out var carBackLeft, out var carBackRight); var initialRotation = GetRelativeCarRotation(player); if (!Physics.Raycast(carMiddle + initialRotation * carFrontLeft, Vector3.down, out var frontLeftHit, DownwardRaycastDistance, RaycastLayers, QueryTriggerInteraction.Ignore) || !Physics.Raycast(carMiddle + initialRotation * carFrontRight, Vector3.down, out var frontRightHit, DownwardRaycastDistance, RaycastLayers, QueryTriggerInteraction.Ignore) || !Physics.Raycast(carMiddle + initialRotation * carBackLeft, Vector3.down, out var backLeftHit, DownwardRaycastDistance, RaycastLayers, QueryTriggerInteraction.Ignore) || !Physics.Raycast(carMiddle + initialRotation * carBackRight, Vector3.down, out var backRightHit, DownwardRaycastDistance, RaycastLayers, QueryTriggerInteraction.Ignore)) { position = Vector3.zero; rotation = Quaternion.identity; return false; } // Rotate the car relative to the hit positions. rotation = Quaternion.LookRotation((frontLeftHit.point - backLeftHit.point), Vector3.up) * Quaternion.Euler(0, 0, (frontLeftHit.point - frontRightHit.point).y * 30); // Spawn in the midpoint between the front and back hits. position = Vector3.Lerp(frontLeftHit.point, backRightHit.point, 0.5f); return true; } private static void DetermineCarPositionAndRotation(BasePlayer player, int numSockets, out Vector3 position, out Quaternion rotation) { if (!TryGetIdealCarPositionAndRotation(player, numSockets, out position, out rotation)) { position = GetFixedCarPosition(player); rotation = GetRelativeCarRotation(player); } } private static Quaternion GetRelativeCarRotation(BasePlayer player) { return Quaternion.Euler(0, player.GetNetworkRotation().eulerAngles.y - 90, 0); } private static void AddInitialModules(ModularCar car, int[] ModuleIDs) { for (var socketIndex = 0; socketIndex < car.TotalSockets; socketIndex++) { var desiredItemID = ModuleIDs[socketIndex]; // We are using 0 to represent an empty socket which we skip. if (desiredItemID != 0) { var moduleItem = ItemManager.CreateByItemID(desiredItemID); if (moduleItem != null) { car.TryAddModule(moduleItem, socketIndex); } } } } private static void UpdateCarModules(ModularCar car, int[] moduleIDs) { // Phase 1: Remove all modules that don't match the desired preset. // This is done first since some modules take up two sockets. for (var socketIndex = 0; socketIndex < car.TotalSockets; socketIndex++) { var desiredItemID = moduleIDs[socketIndex]; var existingItem = car.Inventory.ModuleContainer.GetSlot(socketIndex); if (existingItem != null && existingItem.info.itemid != desiredItemID) { existingItem.RemoveFromContainer(); existingItem.Remove(); } } // Phase 2: Add the modules that are missing. for (var socketIndex = 0; socketIndex < car.TotalSockets; socketIndex++) { var desiredItemID = moduleIDs[socketIndex]; var existingItem = car.Inventory.ModuleContainer.GetSlot(socketIndex); // We are using 0 to represent an empty socket which we skip. if (existingItem == null && desiredItemID != 0) { var moduleItem = ItemManager.CreateByItemID(desiredItemID); if (moduleItem != null) { car.TryAddModule(moduleItem, socketIndex); } } } } private static List AddEngineItemsAndReturnRemaining(ModularCar car, List engineItems) { var itemsByType = engineItems .GroupBy(item => item.info.GetComponent().engineItemType) .ToDictionary( grouping => grouping.Key, grouping => grouping.OrderByDescending(item => item.info.GetComponent().tier).ToList() ); foreach (var module in car.AttachedModuleEntities) { var engineStorage = (module as VehicleModuleEngine)?.GetContainer() as EngineStorage; if (engineStorage == null) continue; for (var slotIndex = 0; slotIndex < engineStorage.inventory.capacity; slotIndex++) { var engineItemType = engineStorage.slotTypes[slotIndex]; if (!itemsByType.ContainsKey(engineItemType)) continue; var itemsOfType = itemsByType[engineItemType]; var existingItem = engineStorage.inventory.GetSlot(slotIndex); if (existingItem != null || itemsOfType.Count == 0) continue; itemsOfType[0].MoveToContainer(engineStorage.inventory, slotIndex, allowStack: false); itemsOfType.RemoveAt(0); } } return itemsByType.Values.SelectMany(x => x).ToList(); } private static void AddUpgradeOrRepairEngineParts(EngineStorage engineStorage, int desiredTier) { var inventory = engineStorage.inventory; if (inventory == null) return; // Ignore if the engine storage is locked, since it must be controlled by another plugin. if (inventory.IsLocked()) return; for (var i = 0; i < inventory.capacity; i++) { var item = inventory.GetSlot(i); if (item != null) { var component = item.info.GetComponent(); if (component != null && component.tier < desiredTier) { item.RemoveFromContainer(); item.Remove(); TryAddEngineItem(engineStorage, i, desiredTier); } else { item.condition = item.maxCondition; } } else if (desiredTier > 0) { TryAddEngineItem(engineStorage, i, desiredTier); } } } private static bool TryAddEngineItem(EngineStorage engineStorage, int slot, int tier) { if (!engineStorage.allEngineItems.TryGetItem(tier, engineStorage.slotTypes[slot], out var output)) return false; var component = output.GetComponent(); var item = ItemManager.Create(component); if (item == null) return false; item.condition = component.condition.max; item.MoveToContainer(engineStorage.inventory, slot, allowStack: false); return true; } private static List ExtractEnginePartsAboveTierAndDeleteRest(ModularCar car, int tier) { var extractedEngineParts = new List(); foreach (var module in car.AttachedModuleEntities) { var engineStorage = (module as VehicleModuleEngine)?.GetContainer() as EngineStorage; if (engineStorage == null) continue; var inventory = engineStorage.inventory; // Ignore if the engine storage is locked, since it must be controlled by another plugin. if (inventory.IsLocked()) continue; for (var i = 0; i < inventory.capacity; i++) { var item = inventory.GetSlot(i); if (item == null) continue; var component = item.info.GetComponent(); if (component == null) continue; item.RemoveFromContainer(); if (component.tier > tier) extractedEngineParts.Add(item); else item.Remove(); } } return extractedEngineParts; } private static void GiveItemsToPlayerOrDrop(BasePlayer player, List itemList) { var itemsToDrop = new List(); foreach (var item in itemList) { if (!player.inventory.GiveItem(item)) { itemsToDrop.Add(item); } } if (itemsToDrop.Count > 0) { DropEngineParts(player, itemsToDrop); } } private static void DropEngineParts(BasePlayer player, List itemList) { if (itemList.Count == 0) return; var position = player.GetDropPosition(); if (itemList.Count == 1) { itemList[0].Drop(position, player.GetDropVelocity()); return; } var container = GameManager.server.CreateEntity(ItemDropPrefab, position, player.GetNetworkRotation()) as DroppedItemContainer; if (container == null) return; container.playerName = $"{player.displayName}'s Engine Parts"; // 4 large engines * 8 parts (each damaged) = 32 max engine parts. // This fits within the standard max size of 36. var capacity = Math.Min(itemList.Count, container.maxItemCount); container.inventory = new ItemContainer(); container.inventory.ServerInitialize(null, capacity); container.inventory.GiveUID(); container.inventory.entityOwner = container; container.inventory.SetFlag(ItemContainer.Flag.NoItemInput, true); foreach (var item in itemList) { if (!item.MoveToContainer(container.inventory)) { item.DropAndTossUpwards(position); } } container.ResetRemovalTime(); container.SetVelocity(player.GetDropVelocity()); container.Spawn(); } private static void FixCar(ModularCar car, int fuelAmount, int enginePartsTier) { car.SetHealth(car.MaxHealth()); car.SendNetworkUpdate(); AddOrRestoreFuel(car, fuelAmount); foreach (var module in car.AttachedModuleEntities) { module.SetHealth(module.MaxHealth()); module.SendNetworkUpdate(); var engineModule = module as VehicleModuleEngine; if (engineModule != null) { var engineStorage = engineModule.GetContainer() as EngineStorage; AddUpgradeOrRepairEngineParts(engineStorage, enginePartsTier); engineModule.RefreshPerformanceStats(engineStorage); } } } private static void ReviveCar(ModularCar car) { car.lifestate = BaseCombatEntity.LifeState.Alive; car.repair.enabled = true; foreach (var module in car.AttachedModuleEntities) { module.repair.enabled = true; } } private static void AddOrRestoreFuel(ModularCar car, int specifiedFuelAmount) { if (car.GetFuelSystem() is not EntityFuelSystem fuelSystem) return; var fuelContainer = fuelSystem.GetFuelContainer(); var targetFuelAmount = specifiedFuelAmount == -1 ? fuelContainer.allowedItem.stackable : specifiedFuelAmount; if (targetFuelAmount == 0) return; var fuelItem = fuelContainer.inventory.FindItemByItemID(fuelContainer.allowedItem.itemid); if (fuelItem == null) { fuelContainer.inventory.AddItem(fuelContainer.allowedItem, targetFuelAmount); } else if (fuelItem.amount < targetFuelAmount) { fuelItem.amount = targetFuelAmount; fuelItem.MarkDirty(); } } private bool TryReleaseCarFromLift(ModularCar car) { if (!TryFindCarLift(car, out var lift)) return false; // Disable the lift for a bit, to prevent it from grabbing the car back. lift.enabled = false; lift.ReleaseOccupant(); lift.Invoke(() => lift.enabled = true, 0.5f); return true; } private bool TryFindCarLift(ModularCar car, out ModularCarGarage lift) { if (Physics.RaycastNonAlloc(car.transform.position, car.transform.right, _raycastBuffer, 2, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore) > 0) { lift = _raycastBuffer[0].GetEntity() as ModularCarGarage; if (lift != null && lift.carOccupant == car) return true; } if (Physics.RaycastNonAlloc(car.transform.position, car.transform.right * -1, _raycastBuffer, 2, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore) > 0) { lift = _raycastBuffer[0].GetEntity() as ModularCarGarage; if (lift != null && lift.carOccupant == car) return true; } lift = null; return false; } private static void DismountAllPlayersFromCar(ModularCar car) { // Dismount seated players. if (car.AnyMounted()) car.DismountAllPlayers(); // Dismount players standing on flatbed modules. foreach (var module in car.AttachedModuleEntities) { foreach (var child in module.children.ToList()) { if (child is BasePlayer player) { player.SetParent(null, worldPositionStays: true); } } } } private static void MaybeFillTankerModules(ModularCar car, int specifiedLiquidAmount) { if (specifiedLiquidAmount == 0) return; foreach (var module in car.AttachedModuleEntities) { var liquidContainer = (module as VehicleModuleStorage)?.GetContainer() as LiquidContainer; if (liquidContainer == null) continue; if (FillLiquidContainer(liquidContainer, specifiedLiquidAmount) && _pluginConfig.EnableEffects) { Effect.server.Run(TankerFilledEffectPrefab, module.transform.position); } } } private static bool FillLiquidContainer(LiquidContainer liquidContainer, int specifiedAmount) { var targetAmount = specifiedAmount == -1 ? liquidContainer.maxStackSize : specifiedAmount; var defaultItem = liquidContainer.defaultLiquid; var existingItem = liquidContainer.GetLiquidItem(); if (existingItem == null) { liquidContainer.inventory.AddItem(defaultItem, targetAmount); return true; } if (existingItem.info.itemid != defaultItem.itemid) { // Remove other liquid such as salt water. existingItem.RemoveFromContainer(); existingItem.Remove(); liquidContainer.inventory.AddItem(defaultItem, targetAmount); return true; } if (existingItem.amount >= targetAmount) // Nothing added in this case. return false; existingItem.amount = targetAmount; existingItem.MarkDirty(); return true; } private static void MaybePlayCarRepairEffects(ModularCar car) { if (!_pluginConfig.EnableEffects) return; if (car.AttachedModuleEntities.Count > 0) { foreach (var module in car.AttachedModuleEntities) { Effect.server.Run(RepairEffectPrefab, module.transform.position); } } else { Effect.server.Run(RepairEffectPrefab, car.transform.position); } } private static int Clamp(int x, int min, int max) { return Math.Min(max, Math.Max(min, x)); } private bool IsPlayerCar(ModularCar car) { return _pluginData.PlayerCars.ContainsValue(car.net.ID.Value); } private ModularCar FindPlayerCar(IPlayer player) { if (!_pluginData.PlayerCars.ContainsKey(player.Id)) return null; var car = BaseNetworkable.serverEntities.Find(new NetworkableId(_pluginData.PlayerCars[player.Id])) as ModularCar; // Just in case the car was removed and that somehow wasn't detected sooner. // This could happen if the data file somehow got out of sync for instance. if (car == null) { _pluginData.UnregisterCar(player.Id); } return car; } private bool HasSufficientSpace(BasePlayer player, int numSockets, Vector3 desiredPosition, Quaternion rotation) { var carExtents = GetCarExtents(numSockets); var carCenterPoint = desiredPosition + rotation * new Vector3(0, carExtents.y); // Need some extra height for the boxcast to allow spawning on a lift since lifts are construction. // Cars can't be spawned on sleepers. // Cars can still be spawned below ceiling lights. carCenterPoint.y += 0.3f; return Physics.BoxCastNonAlloc(carCenterPoint, carExtents, rotation * Vector3.forward, _raycastBuffer, rotation, 0.1f, BoxcastLayers, QueryTriggerInteraction.Ignore) == 0; } private int GetPlayerAllowedFreshWater(string userId) { return permission.UserHasPermission(userId, PermissionAutoFillTankers) && GetPlayerConfig(userId).Settings.AutoFillTankers ? _pluginConfig.FreshWaterAmount : 0; } private int GetPlayerAllowedFuel(string userId) { return permission.UserHasPermission(userId, PermissionAutoFuel) ? _pluginConfig.FuelAmount : 0; } private int GetPlayerEnginePartsTier(string userId) { if (permission.UserHasPermission(userId, PermissionEnginePartsTier3)) return 3; if (permission.UserHasPermission(userId, PermissionEnginePartsTier2)) return 2; if (permission.UserHasPermission(userId, PermissionEnginePartsTier1)) return 1; return 0; } private ushort GetPlayerMaxAllowedCarSockets(string userId) { if (permission.UserHasPermission(userId, PermissionSpawnSockets4)) return 4; if (permission.UserHasPermission(userId, PermissionSpawnSockets3)) return 3; if (permission.UserHasPermission(userId, PermissionSpawnSockets2)) return 2; return 0; } private void SpawnRandomCarForPlayer(IPlayer player, int desiredSockets) { var basePlayer = player.Object as BasePlayer; if (!VerifySufficientSpace(player, desiredSockets, out var spawnPosition, out var rotation) || SpawnMyCarWasBlocked(basePlayer)) return; var carOptions = new RandomCarOptions(player.Id, desiredSockets); var car = SpawnCar(carOptions, spawnPosition, rotation, basePlayer, shouldTrackCar: true); if (car == null) return; ReplyToPlayer(player, "Command.Spawn.Success"); } private void SpawnPresetCarForPlayer(IPlayer player, SimplePreset preset) { if (preset.NumSockets > GetPlayerMaxAllowedCarSockets(player.Id)) { ReplyToPlayer(player, "Generic.Error.NoPermissionToPresetSocketCount", preset.Name, preset.NumSockets); return; } var basePlayer = player.Object as BasePlayer; if (!VerifySufficientSpace(player, preset.NumSockets, out var spawnPosition, out var rotation) || SpawnMyCarWasBlocked(basePlayer)) return; var carOptions = new PresetCarOptions(player.Id, preset.ModuleIDs); var car = SpawnCar(carOptions, spawnPosition, rotation, basePlayer, shouldTrackCar: true); if (car == null) return; ReplyToPlayer(player, "Command.Spawn.Success.Preset", preset.Name); if (preset != null) { MaybePlayCarRepairEffects(car); } } private ModularCar SpawnCar(BaseCarOptions options, Vector3 position, Quaternion rotation, BasePlayer player = null, bool shouldTrackCar = false) { var numSockets = options.Length; string prefabName; if (numSockets == 4) { prefabName = PrefabSockets4; } else if (numSockets == 3) { prefabName = PrefabSockets3; } else if (numSockets == 2) { prefabName = PrefabSockets2; } else { return null; } var car = GameManager.server.CreateEntity(prefabName, position, rotation) as ModularCar; if (car == null) return null; var presetOptions = options as PresetCarOptions; if (presetOptions != null) { car.spawnSettings.useSpawnSettings = false; } if (player != null) { car.OwnerID = player.userID; } car.Spawn(); if (presetOptions != null) { AddInitialModules(car, presetOptions.NormalizedModuleIDs); } if (shouldTrackCar && player != null) { _pluginData.StartCooldown(player.UserIDString, CooldownType.Spawn, save: false); _pluginData.RegisterCar(player.UserIDString, car); } // Force all modules to be processed and registered in AttachedModuleEntities. // This allows plugins to easily interact with the module entities such as to add engine parts. foreach (var entry in car.moduleAddActions.ToList()) { entry.Key.CancelInvoke(entry.Value); entry.Value.Invoke(); } FixCar(car, options.FuelAmount, options.EnginePartsTier); MaybeFillTankerModules(car, options.FreshWaterAmount); if (options.CodeLock && VehicleDeployedLocks != null) { VehicleDeployedLocks.Call("API_DeployCodeLock", car, player); } if (!options.CodeLock && options.KeyLock && VehicleDeployedLocks != null) { VehicleDeployedLocks.Call("API_DeployKeyLock", car, player); } return car; } private bool ShouldTryAddCodeLockForPlayer(string userId) { return permission.UserHasPermission(userId, PermissionAutoCodeLock) && GetPlayerConfig(userId).Settings.AutoCodeLock; } private bool ShouldTryAddKeyLockForPlayer(string userId) { return permission.UserHasPermission(userId, PermissionAutoKeyLock) && GetPlayerConfig(userId).Settings.AutoKeyLock; } private int[] ValidateModules(object[] moduleArray) { ItemManager.Initialize(); var moduleIDList = new List(); foreach (var module in moduleArray) { ItemDefinition itemDef; if (module is int or long) { var moduleInt = module is long l ? Convert.ToInt32(l) : (int)module; if (moduleInt == 0) { moduleIDList.Add(0); continue; } itemDef = ItemManager.FindItemDefinition(moduleInt); } else if (module is string s) { if (int.TryParse(s, out var parsedItemId)) { if (parsedItemId == 0) { moduleIDList.Add(0); continue; } itemDef = ItemManager.FindItemDefinition(parsedItemId); } else itemDef = ItemManager.FindItemDefinition(s); } else { LogWarning("Unable to parse module id or name: '{0}'", module); continue; } if (itemDef == null) { LogWarning("No item definition found for: '{0}'", module); continue; } var vehicleModule = itemDef.GetComponent(); if (vehicleModule == null) { LogWarning("No vehicle module found for item: '{0}'", module); continue; } moduleIDList.Add(itemDef.itemid); // Normalize module IDs by adding 0s after the module if it takes multiple sockets. for (var i = 0; i < vehicleModule.SocketsTaken - 1; i++) { moduleIDList.Add(0); } } return moduleIDList.ToArray(); } #endregion #region Data Management private class PluginData : SimplePresetManager { [JsonProperty("playerCars")] public Dictionary PlayerCars = new(); [JsonProperty("Cooldowns")] public CooldownManager Cooldowns = new(); public override List Presets { get; set; } public bool ShouldSerializePresets() => false; public static PluginData LoadData() { return Interface.Oxide.DataFileSystem.ReadObject(_pluginInstance.Name); } public override void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(_pluginInstance.Name, this); } public void RegisterCar(string userId, ModularCar car) { PlayerCars.Add(userId, car.net.ID.Value); SaveData(); } public void UnregisterCar(string userId) { PlayerCars.Remove(userId); SaveData(); } public long GetRemainingCooldownSeconds(string userId, CooldownType cooldownType) { if (!Cooldowns.GetCooldownMap(cooldownType).TryGetValue(userId, out var cooldownStart)) return 0; var cooldownSeconds = _pluginConfig.Cooldowns.GetSeconds(cooldownType); return cooldownStart + cooldownSeconds - DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } public void StartCooldown(string userId, CooldownType cooldownType, bool save = true) { if (_pluginConfig.Cooldowns.GetSeconds(cooldownType) <= 0) return; Cooldowns.GetCooldownMap(cooldownType)[userId] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (save) { SaveData(); } } } private class CommonPresets : SimplePresetManager { private static string Filename => $"{_pluginInstance.Name}_CommonPresets"; public static CommonPresets LoadData(PluginData pluginData) { var data = Interface.Oxide.DataFileSystem.ReadObject(Filename); if (pluginData.Presets != null) { if (data.Presets == null || data.Presets.Count == 0) { _pluginInstance.LogWarning($"Migrating common presets to separate data file: {Filename}.json."); data.Presets = pluginData.Presets.ToList(); data.SaveData(); } else { _pluginInstance.LogWarning($"Deleting common presets from main data file since they appear to have already been migrated to a separate data file: {Filename}.json."); } pluginData.Presets.Clear(); pluginData.SaveData(); } return data; } public override void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(Filename, this); } } private PlayerConfig GetPlayerConfig(IPlayer player) { return GetPlayerConfig(player.Id); } private PlayerConfig GetPlayerConfig(string userId) { if (_playerConfigsMap.ContainsKey(userId)) return _playerConfigsMap[userId]; var config = PlayerConfig.Get(Name, userId); _playerConfigsMap.Add(userId, config); return config; } private enum CooldownType { Spawn, Fetch, Load, Fix } private class CooldownManager { [JsonProperty("Spawn")] private Dictionary Spawn = new(); [JsonProperty("Fetch")] private Dictionary Fetch = new(); [JsonProperty("LoadPreset")] private Dictionary LoadPreset = new(); [JsonProperty("Fix")] private Dictionary Fix = new(); public Dictionary GetCooldownMap(CooldownType cooldownType) { switch (cooldownType) { case CooldownType.Spawn: return Spawn; case CooldownType.Fetch: return Fetch; case CooldownType.Load: return LoadPreset; case CooldownType.Fix: return Fix; default: _pluginInstance.LogWarning($"Cooldown not implemented for {cooldownType}"); return null; } } public void ClearAll() { Spawn.Clear(); Fetch.Clear(); LoadPreset.Clear(); Fix.Clear(); } } private abstract class SimplePresetManager { public static Func MatchPresetName(string presetName) => preset => preset.Name.Equals(presetName, StringComparison.CurrentCultureIgnoreCase); [JsonProperty("Presets")] public virtual List Presets { get; set; } = new(); public SimplePreset FindPreset(string presetName) { return Presets.FirstOrDefault(MatchPresetName(presetName)); } public List FindMatchingPresets(string presetName) { return Presets.Where(preset => preset.Name.IndexOf(presetName, StringComparison.CurrentCultureIgnoreCase) >= 0).ToList(); } public void SavePreset(SimplePreset newPreset) { Presets.Add(newPreset); SaveData(); } public void UpdatePreset(SimplePreset newPreset) { var presetIndex = Presets.FindIndex(new Predicate(MatchPresetName(newPreset.Name))); if (presetIndex == -1) return; Presets[presetIndex] = newPreset; SaveData(); } public void RenamePreset(SimplePreset preset, string newName) { preset.Name = newName; SaveData(); } public void DeletePreset(SimplePreset preset) { Presets.Remove(preset); SaveData(); } public abstract void SaveData(); } private class PlayerConfig : SimplePresetManager { public static PlayerConfig Get(string dirPath, string ownerID) { var filepath = $"{dirPath}/{ownerID}"; var config = Interface.Oxide.DataFileSystem.ExistsDatafile(filepath) ? Interface.Oxide.DataFileSystem.ReadObject(filepath) : new PlayerConfig(ownerID); config.Filepath = filepath; return config; } [JsonIgnore] private string Filepath; [JsonProperty("OwnerID")] public string OwnerID { get; } [JsonProperty("Settings")] public PlayerSettings Settings = new(); public PlayerConfig(string ownerID) { OwnerID = ownerID; } public override void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(Filepath, this); } } private class SimplePreset { public static SimplePreset FromCar(ModularCar car, string presetName) { return new SimplePreset { Name = presetName, ModuleIDs = GetCarModuleIDs(car) }; } [JsonProperty("Name")] public string Name; [JsonProperty("ModuleIDs")] public int[] ModuleIDs; [JsonIgnore] public int NumSockets => ModuleIDs.Length; } private class PlayerSettings { [JsonProperty("AutoCodeLock")] public bool AutoCodeLock; [JsonProperty("AutoKeyLock")] public bool AutoKeyLock; [JsonProperty("AutoFillTankers")] public bool AutoFillTankers; } #endregion #region Configuration private void MigrateConfig() { if (_pluginConfig.ValidateServerPresets()) { LogWarning("Performing automatic config migration."); SaveConfig(); } } private class Configuration : SerializableConfiguration { [JsonProperty("CanSpawnWhileBuildingBlocked")] public bool CanSpawnBuildingBlocked = false; [JsonProperty("CanFetchWhileBuildingBlocked")] public bool CanFetchBuildingBlocked = false; [JsonProperty("CanFetchWhileOccupied")] public bool CanFetchOccupied = false; [JsonProperty("CanDespawnWhileOccupied")] public bool CanDespawnOccupied = false; [JsonProperty("DismountPlayersOnFetch")] public bool DismountPlayersOnFetch = true; [JsonProperty("FuelAmount")] public int FuelAmount = 500; [JsonProperty("FreshWaterAmount")] public int FreshWaterAmount = -1; [JsonProperty("MaxPresetsPerPlayer")] public int MaxPresetsPerPlayer = 10; [JsonProperty("EnableEffects")] public bool EnableEffects = true; [JsonProperty("DisallowedMonuments")] private string[] DisallowedMonuments = Array.Empty(); [JsonProperty("Cooldowns")] public CooldownConfig Cooldowns = new(); [JsonProperty("Presets")] public ServerPreset[] Presets = Array.Empty(); [JsonIgnore] public bool HasMonumentRestriction => DisallowedMonuments?.Length > 0; public ServerPreset FindPreset(string name) { var nameLower = name.ToLower(); foreach (var preset in Presets) { if (preset.Name.ToLower() == nameLower) { return preset; } } return null; } public bool ValidateServerPresets() { var changed = false; foreach (var preset in Presets) { if (preset.Options.ValidateModules()) changed = true; } return changed; } public bool IsMonumentAllowed(string monumentName) { if (DisallowedMonuments.Length == 0) return true; foreach (var disallowedMonumentName in DisallowedMonuments) { if (monumentName.IndexOf(disallowedMonumentName, StringComparison.OrdinalIgnoreCase) >= 0) return false; } return true; } } private class ServerPreset { [JsonProperty("Name")] public string Name; [JsonProperty("Options")] public ServerPresetOptions Options; } private abstract class BaseCarOptions { private int _enginePartsTier; [JsonProperty("CodeLock", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool CodeLock; [JsonProperty("EnginePartsTier", DefaultValueHandling = DefaultValueHandling.Ignore)] public int EnginePartsTier { get => _enginePartsTier; set => _enginePartsTier = Clamp(value, 0, 3); } [JsonProperty("FreshWaterAmount", DefaultValueHandling = DefaultValueHandling.Ignore)] public int FreshWaterAmount; [JsonProperty("FuelAmount", DefaultValueHandling = DefaultValueHandling.Ignore)] public int FuelAmount; [JsonProperty("KeyLock", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool KeyLock; [JsonIgnore] public abstract int Length { get; } public BaseCarOptions() { } public BaseCarOptions(string userId) { CodeLock = _pluginInstance.ShouldTryAddCodeLockForPlayer(userId); KeyLock = _pluginInstance.ShouldTryAddKeyLockForPlayer(userId); EnginePartsTier = _pluginInstance.GetPlayerEnginePartsTier(userId); FuelAmount = _pluginInstance.GetPlayerAllowedFuel(userId); FreshWaterAmount = _pluginInstance.GetPlayerAllowedFreshWater(userId); } } private class PresetCarOptions : BaseCarOptions { [JsonProperty("ModuleIDs")] public virtual int[] NormalizedModuleIDs { get; set; } = Array.Empty(); [JsonIgnore] public override int Length => NormalizedModuleIDs?.Length ?? 0; // Empty constructor needed for deserialization. public PresetCarOptions() { } public PresetCarOptions(string userId, int[] moduleIDs) : base(userId) { NormalizedModuleIDs = moduleIDs; } } private class RandomCarOptions : BaseCarOptions { public int NumSockets; public override int Length => NumSockets; public RandomCarOptions(string userId, int numSockets) : base(userId) { NumSockets = numSockets; } } private class ServerPresetOptions : PresetCarOptions { // Override so we can avoid serializing it. public override int[] NormalizedModuleIDs { get; set; } // Hidden from config. public bool ShouldSerializeNormalizedModuleIDs() => false; [JsonProperty("Modules")] public object[] Modules; // Return value indicates whether the config was changed. public bool ValidateModules() { // Give precedence to "Modules". if (Modules != null) { NormalizedModuleIDs = _pluginInstance.ValidateModules(Modules); } else if (NormalizedModuleIDs != null) { // Resave the config with the field renamed to Modules. // Must do this before normalizing so that no extra 0's are added. Modules = NormalizedModuleIDs.Cast().ToArray(); NormalizedModuleIDs = NormalizeModuleIDs(NormalizedModuleIDs); return true; } return false; } private int[] NormalizeModuleIDs(int[] moduleIDs) { ItemManager.Initialize(); var moduleIDList = moduleIDs.ToList(); for (var i = 0; i < moduleIDList.Count; i++) { if (moduleIDList[i] != 0) { // Add a 0 after each module that takes 2 sockets. // This is more user-friendly than requiring people to add the 0s themselves. var itemDefinition = ItemManager.FindItemDefinition(moduleIDList[i]); var socketsTaken = itemDefinition.GetComponent()?.SocketsTaken ?? 1; if (socketsTaken == 2) moduleIDList.Insert(i + 1, 0); } } return moduleIDList.ToArray(); } } private class CooldownConfig { [JsonProperty("SpawnCarSeconds")] public long SpawnSeconds = 3600; [JsonProperty("FetchCarSeconds")] public long FetchSeconds = 600; [JsonProperty("LoadPresetSeconds")] public long LoadPresetSeconds = 3600; [JsonProperty("FixCarSeconds")] public long FixSeconds = 3600; public long GetSeconds(CooldownType cooldownType) { switch (cooldownType) { case CooldownType.Spawn: return SpawnSeconds; case CooldownType.Fetch: return FetchSeconds; case CooldownType.Load: return LoadPresetSeconds; case CooldownType.Fix: return FixSeconds; default: _pluginInstance.LogWarning($"Cooldown not implemented for {cooldownType}"); return 0; } } } private Configuration GetDefaultConfig() => new(); #endregion #region Configuration Helpers private class SerializableConfiguration { public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary; } 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() .ToDictionary(prop => prop.Name, prop => ToObject(prop.Value)); case JTokenType.Array: return token.Select(ToObject).ToList(); default: return ((JValue)token).Value; } } } private bool MaybeUpdateConfig(SerializableConfiguration config) { var currentWithDefaults = config.ToDictionary(); var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value); return MaybeUpdateConfigDict(currentWithDefaults, currentRaw); } private bool MaybeUpdateConfigDict(Dictionary currentWithDefaults, Dictionary currentRaw) { var changed = false; foreach (var key in currentWithDefaults.Keys) { if (currentRaw.TryGetValue(key, out var currentRawValue)) { var currentDictValue = currentRawValue as Dictionary; if (currentWithDefaults[key] is Dictionary 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() => _pluginConfig = GetDefaultConfig(); protected override void LoadConfig() { base.LoadConfig(); try { _pluginConfig = Config.ReadObject(); if (_pluginConfig == null) { throw new JsonException(); } if (MaybeUpdateConfig(_pluginConfig)) { 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(_pluginConfig, true); } #endregion #region Localization private string BooleanToLocalizedString(IPlayer player, bool value) { return value ? GetMessage(player, "Generic.Setting.On") : GetMessage(player, "Generic.Setting.Off"); } private void ReplyToPlayer(IPlayer player, string messageName, params object[] args) { player.Reply(string.Format(GetMessage(player, messageName), args)); } private void ChatMessage(BasePlayer player, string messageName, params object[] args) { player.ChatMessage(string.Format(GetMessage(player.IPlayer, messageName), args)); } private string GetMessage(IPlayer player, string messageName, params object[] args) { var message = lang.GetMessage(messageName, this, player.Id); return args.Length > 0 ? string.Format(message, args) : message; } protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["Generic.Setting.On"] = "ON", ["Generic.Setting.Off"] = "OFF", ["Generic.Error.NoPermission"] = "You don't have permission to use this command.", ["Generic.Error.LocationRestricted"] = "Error: Cannot do that here.", ["Generic.Error.BuildingBlocked"] = "Error: Cannot do that while building blocked.", ["Generic.Error.NoPresets"] = "You don't have any saved presets.", ["Generic.Error.NoCommonPresets"] = "There are no common presets.", ["Generic.Error.CarNotFound"] = "Error: You need a car to do that.", ["Generic.Error.CarOccupied"] = "Error: Cannot do that while your car is occupied.", ["Generic.Error.Cooldown"] = "Please wait {0}s and try again.", ["Generic.Error.NoPermissionToPresetSocketCount"] = "Error: You don't have permission to use preset {0} because it requires {1} sockets.", ["Generic.Error.PresetNotFound"] = "Error: Preset {0} not found.", ["Generic.Error.PresetMultipleMatches"] = "Error: Multiple presets found matching {0}. Use mycar list to view your presets.", ["Generic.Error.PresetAlreadyTaken"] = "Error: Preset {0} is already taken.", ["Generic.Error.PresetNameLength"] = "Error: Preset name may not be longer than {0} characters.", ["Generic.Error.InsufficientSpace"] = "Error: Not enough space.", ["Generic.Info.CarDestroyed"] = "Your modular car was destroyed.", ["Generic.Info.PartsRecovered"] = "Recovered engine components were added to your inventory or dropped in front of you.", ["Command.Spawn.Error.SocketSyntax"] = "Syntax: mycar <2|3|4>", ["Command.Spawn.Error.CarAlreadyExists"] = "Error: You already have a car.", ["Command.Spawn.Error.CarAlreadyExists.Help"] = "Try mycar fetch or mycar help.", ["Command.Spawn.Success"] = "Here is your modular car.", ["Command.Spawn.Success.Preset"] = "Here is your modular car from preset {0}.", ["Command.Fix.Success"] = "Your car was fixed.", ["Command.Fetch.Error.StuckOnLift"] = "Error: Unable to fetch your car from its lift.", ["Command.Fetch.Error.StuckOnLift.Help"] = "You can use mycar destroy to destroy it.", ["Command.Fetch.Success"] = "Here is your modular car.", ["Command.SavePreset.Error.TooManyPresets"] = "Error: You may not have more than {0} presets. You may delete another preset and try again. See mycar help.", ["Command.SavePreset.Error.PresetAlreadyExists"] = "Error: Preset {0} already exists. Use mycar update {0} to update it.", ["Command.SavePreset.Success"] = "Saved car as {0} preset.", ["Command.UpdatePreset.Success"] = "Updated {0} preset with current module configuration.", ["Command.LoadPreset.Error.SocketCount"] = "Error: Unable to load {0} preset ({1} sockets) because your car has {2} sockets.", ["Command.LoadPreset.Success"] = "Loaded {0} preset onto your car.", ["Command.DeletePreset.Success"] = "Deleted {0} preset.", ["Command.RenamePreset.Error.Syntax"] = "Syntax: mycar rename ", ["Command.RenamePreset.Success"] = "Renamed {0} preset to {1}", ["Command.List"] = "Your saved modular car presets:", ["Command.List.Item"] = "{0} ({1} sockets)", ["Command.Common.List"] = "Common modular car presets:", ["Command.Common.Error.Syntax"] = "Try mycar help", ["Command.Common.LoadPreset.Error.Syntax"] = "Syntax: mycar common load ", ["Command.Common.SavePreset.Error.Syntax"] = "Syntax: mycar common save ", ["Command.Common.SavePreset.Error.PresetAlreadyExists"] = "Error: Common preset {0} already exists. Use mycar common update {0} to update it.", ["Command.Common.UpdatePreset.Error.Syntax"] = "Syntax: mycar common update ", ["Command.Common.RenamePreset.Error.Syntax"] = "Syntax: mycar common rename ", ["Command.Common.DeletePreset.Error.Syntax"] = "Syntax: mycar common delete ", ["Command.ToggleAutoCodeLock.Success"] = "AutoCodeLock set to {0}", ["Command.ToggleAutoKeyLock.Success"] = "AutoKeyLock set to {0}", ["Command.ToggleAutoFillTankers.Success"] = "AutoFillTankers set to {0}", ["Command.Give.Error.Syntax"] = "Syntax: givecar ", ["Command.Give.Error.PlayerNotFound"] = "Error: Player {0} not found.", ["Command.Give.Error.PresetTooFewModules"] = "Error: Preset {0} has too few modules ({1}).", ["Command.Give.Error.PresetTooManyModules"] = "Error: Preset {0} has too many modules ({1}).", ["Command.Give.Success"] = "Modular car given to {0} from preset {1}.", ["Command.Help"] = "SpawnModularCar Command Usages", ["Command.Help.Spawn.Basic"] = "mycar - Spawn a random car with max allowed sockets", ["Command.Help.Spawn.Basic.PresetsAllowed"] = "mycar - Spawn a car using your default preset if saved, else spawn a random car with max allowed sockets", ["Command.Help.Spawn.Sockets"] = "mycar <2|3|4> - Spawn a random car of desired length", ["Command.Help.Fetch"] = "mycar fetch - Fetch your car", ["Command.Help.Fix"] = "mycar fix - Fix your car", ["Command.Help.Destroy"] = "mycar destroy - Destroy your car", ["Command.Help.Section.PersonalPresets"] = "--- Personal presets ---", ["Command.Help.ListPresets"] = "mycar list - List your saved presets", ["Command.Help.Spawn.Preset"] = "mycar - Spawn a car from a saved preset", ["Command.Help.LoadPreset"] = "mycar load - Load a preset onto your car", ["Command.Help.SavePreset"] = "mycar save - Save your car as a preset", ["Command.Help.UpdatePreset"] = "mycar update - Overwrite a preset", ["Command.Help.RenamePreset"] = "mycar rename - Rename a preset", ["Command.Help.DeletePreset"] = "mycar delete - Delete a preset", ["Command.Help.Section.CommonPresets"] = "--- Common presets ---", ["Command.Help.Common.ListPresets"] = "mycar common list - List common presets", ["Command.Help.Common.Spawn"] = "mycar common - Spawn a car from a common preset", ["Command.Help.Common.LoadPreset"] = "mycar common load - Load a common preset onto your car", ["Command.Help.Common.SavePreset"] = "mycar common save - Save your car as a common preset", ["Command.Help.Common.UpdatePreset"] = "mycar common update - Overwrite a common preset", ["Command.Help.Common.RenamePreset"] = "mycar common rename - Rename a common preset", ["Command.Help.Common.DeletePreset"] = "mycar common delete - Delete a common preset", ["Command.Help.Section.PersonalSettings"] = "--- Personal settings ---", ["Command.Help.ToggleAutoCodeLock"] = "mycar autocodelock - Toggle AutoCodeLock: {0}", ["Command.Help.ToggleAutoKeyLock"] = "mycar autokeylock - Toggle AutoKeyLock: {0}", ["Command.Help.ToggleAutoFillTankers"] = "mycar autofilltankers - Toggle automatic filling of tankers with fresh water: {0}", ["Command.Help.Section.OtherCommands"] = "--- Other commands ---", ["Command.Help.Give"] = "givecar - Spawn a car for the target player from the specified server preset", }, this); } #endregion } }