using Network;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Corpse Location", "nivex/shinnova", "2.3.811")]
[Description("Allows users to locate their latest corpse")]
internal class CorpseLocation : RustPlugin
{
[PluginReference] Plugin ZoneManager, AbandonedBases, RaidableBases;
public enum AmountType { Double, Float, Int }
public enum PlayerType { BasePlayer, String, ULong }
private const string UsePerm = "corpselocation.use";
private const string TPPerm = "corpselocation.tp";
private const string VIPPerm = "corpselocation.vip";
private const string AdminPerm = "corpselocation.admin";
private const string NoCostPerm = "corpselocation.nocost";
private Dictionary<string, Timer> ActiveTimers = new();
private Dictionary<string, Vector3> ReturnLocations = new();
#region Data
public class StoredData
{
public Dictionary<string, string> deaths = new();
public Dictionary<string, int> teleportsRemaining = new();
public StoredData() { }
}
private StoredData storedData = new();
private void NewData()
{
storedData = new();
SaveData();
}
private void LoadData()
{
try
{
storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
}
catch { }
if (storedData == null || storedData.deaths == null)
{
Puts("Corrupted data -- generating new data file");
NewData();
}
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, storedData);
}
#endregion Data
#region Hooks
private void OnNewSave(string filename)
{
NewData();
}
private void OnServerSave()
{
timer.Once(15f, SaveData);
}
private void Unload()
{
SaveData();
}
private void OnServerInitialized()
{
permission.RegisterPermission(UsePerm, this);
permission.RegisterPermission(TPPerm, this);
permission.RegisterPermission(VIPPerm, this);
permission.RegisterPermission(AdminPerm, this);
LoadData();
StartResetTimer();
}
private void OnPlayerRespawned(BasePlayer player)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, UsePerm))
{
return;
}
if (storedData.deaths.TryGetValue(player.UserIDString, out var location))
{
SendCorpseLocation(player, location.ToVector3());
}
}
private void OnEntityDeath(BasePlayer player, HitInfo info)
{
if (player == null || player.IsDestroyed || !player.userID.IsSteamId())
{
return;
}
storedData.deaths[player.UserIDString] = player.transform.position.ToString();
Puts($"{player.displayName} ({player.UserIDString}) died at {player.transform.position}");
}
private void OnEntitySpawned(PlayerCorpse corpse)
{
if (corpse == null)
{
return;
}
string userid = corpse.playerSteamID.ToString();
if (!userid.IsSteamId())
{
return;
}
if (ActiveTimers.Remove(userid, out Timer t))
{
t.Destroy();
}
ActiveTimers[userid] = timer.Repeat(1, config.trackTime, () =>
{
if (corpse != null && !corpse.IsDestroyed)
{
storedData.deaths[userid] = corpse.transform.position.ToString();
}
});
}
private void OnUserPermissionGranted(string id, string permName)
{
if (permName != VIPPerm)
{
return;
}
storedData.teleportsRemaining.Remove(id); // [id] = storedData.teleportsRemaining.TryGetValue(id, out var n) ? n + config.viptpAmount - config.tpAmount : config.viptpAmount;
}
private void OnUserGroupAdded(string id, string groupName)
{
foreach (var permName in permission.GetGroupPermissions(groupName))
{
if (permName == VIPPerm)
{
OnUserPermissionGranted(id, permName);
}
}
}
#endregion Hooks
#region Helpers
private void StartResetTimer()
{
if (!TimeSpan.TryParse(config.resetTime, out var resetTime))
{
Puts("Invalid resetTime format. Using midnight as default.");
resetTime = TimeSpan.Zero;
}
DateTime now = DateTime.Now;
DateTime nextReset = now.Date + resetTime;
if (nextReset <= now)
nextReset = nextReset.AddDays(1);
float time = (float)(nextReset - now).TotalSeconds;
timer.Once(time, () =>
{
foreach (var playerId in storedData.teleportsRemaining.Keys.ToList())
{
bool isVIP = permission.UserHasPermission(playerId, VIPPerm);
if (isVIP && config.viptpAmount == 0) continue;
if (!isVIP && config.tpAmount == 0) continue;
storedData.teleportsRemaining[playerId] = isVIP ? config.viptpAmount : config.tpAmount;
}
SaveData();
Puts("Daily teleports were reset.");
foreach (var player in BasePlayer.activePlayerList)
{
Message(player, "DailyReset");
}
StartResetTimer();
});
}
public bool CanPlayerTeleport(BasePlayer player, Vector3 to)
{
if (config.blockToZM && ZoneManager != null && Convert.ToBoolean(ZoneManager?.Call("HasPlayerFlag", player, "notp")))
{
Message(player, "TeleportBlockedCorpse");
return false;
}
if (config.blockFromBuildBlocked && player.IsBuildingBlocked())
{
Message(player, "TeleportBlockedFrom");
return false;
}
if (config.blockToBuildBlocked && player.IsBuildingBlocked(to, player.transform.rotation, player.bounds))
{
Message(player, "TeleportBlockedTo");
return false;
}
if (config.ignoreRB && RaidableBases != null && Convert.ToBoolean(RaidableBases?.Call("EventTerritory", to)))
{
return true;
}
if (config.ignoreAB && AbandonedBases != null && Convert.ToBoolean(AbandonedBases?.Call("EventTerritory", to)))
{
return true;
}
var ret = Interface.CallHook("CanTeleport", player, to);
if (ret is string str)
{
Player.Message(player, str);
return false;
}
return true;
}
private object OnBlockRaidableBasesTeleport(BasePlayer player, Vector3 to) => config.ignoreRB ? true : (object)null;
private object OnBlockAbandonedBasesTeleport(BasePlayer player, Vector3 to) => config.ignoreAB ? true : (object)null;
private static string PositionToGrid(Vector3 position) => MapHelper.PositionToString(position);
private void SendCorpseLocation(BasePlayer player, Vector3 location)
{
int DistanceToCorpse = Mathf.FloorToInt(Vector3.Distance(player.transform.position, location));
if (config.showGrid)
{
Message(player, "YouDiedGrid", DistanceToCorpse, PositionToGrid(location));
}
else Message(player, "YouDied", DistanceToCorpse);
}
private List<BasePlayer> GetPlayers(string NameOrID)
{
return BasePlayer.allPlayerList.Where(target =>
{
if (target == null)
{
return false;
}
return target.UserIDString == NameOrID || target.displayName.Contains(NameOrID, StringComparison.OrdinalIgnoreCase);
}).ToList();
}
#endregion Helpers
#region Commands
[ChatCommand("where")]
private void whereCommand(BasePlayer player, string command, string[] args)
{
string PlayerID = player.UserIDString;
if (args.Contains("tp") && permission.UserHasPermission(PlayerID, TPPerm))
{
int TPAllowed = config.tpAmount;
if (permission.UserHasPermission(PlayerID, VIPPerm))
{
TPAllowed = config.viptpAmount;
}
int remainingTeleports = 0;
if (TPAllowed > 0)
{
if (!storedData.teleportsRemaining.TryGetValue(PlayerID, out remainingTeleports) || remainingTeleports > TPAllowed)
{
remainingTeleports = TPAllowed;
storedData.teleportsRemaining[PlayerID] = remainingTeleports;
SaveData();
}
}
if (!storedData.deaths.TryGetValue(PlayerID, out string location))
{
Message(player, "UnknownLocation");
return;
}
if (config.blockFromZM && Convert.ToBoolean(ZoneManager?.CallHook("HasPlayerFlag", player, "notp")))
{
Message(player, "TeleportBlockedPlayer");
return;
}
Vector3 destination = location.ToVector3();
if (!CanPlayerTeleport(player, destination))
{
return;
}
if (TPAllowed > 0 && remainingTeleports <= 0)
{
Message(player, "OutOfTeleports");
}
else
{
float tpCd = config.tpCountdown;
if (tpCd > 0)
{
Message(player, "TeleportingIn", tpCd);
}
timer.Once(tpCd, () =>
{
if (!CanPlayerTeleport(player, destination))
{
return;
}
if (!Teleport(player, destination, IsFree(player, args))) return;
Vector3 originalpos = player.transform.position;
player.Invoke(() =>
{
if (config.blockToZM && Convert.ToBoolean(ZoneManager?.Call("HasPlayerFlag", player, "notp")))
{
player.Teleport(originalpos);
Message(player, "TeleportBlockedCorpse");
return;
}
Message(player, "ArrivedAtYourCorpse");
if (config.allowReturn)
{
ReturnLocations[PlayerID] = originalpos;
Message(player, "ReturnAvailable");
}
if (TPAllowed > 0)
{
storedData.teleportsRemaining[PlayerID] = --remainingTeleports;
SaveData();
Message(player, "TeleportsRemaining", remainingTeleports);
}
}, 0.1f);
});
}
return;
}
if (permission.UserHasPermission(PlayerID, UsePerm))
{
if (storedData.deaths.TryGetValue(player.UserIDString, out var location))
{
SendCorpseLocation(player, location.ToVector3());
}
else Message(player, "UnknownLocation");
if (storedData.teleportsRemaining.ContainsKey(player.UserIDString))
{
Message(player, "TeleportsRemaining", storedData.teleportsRemaining[PlayerID]);
}
else
{
int TPAllowed = config.tpAmount;
if (permission.UserHasPermission(PlayerID, VIPPerm))
{
TPAllowed = config.viptpAmount;
}
if (TPAllowed > 0)
{
Message(player, "TeleportsRemaining", TPAllowed);
}
}
}
else Message(player, "NotAllowed");
}
private bool TryPay(BasePlayer player, PaymentMethod m) => m switch
{
{ IsEnabled: false } => true,
_ when player == null => false,
_ => TryPay(player, m.PlayerType, m.AmountType, m.Amount, m.PluginName, m.BalanceHook, m.WithdrawHook, m.CostFormat)
};
private bool TryPay(BasePlayer player, PlayerType playerType, AmountType amountType, double amount, string pluginName, string balanceHook, string withdrawHook, string formattedCost)
{
Plugin plugin = plugins.Find(pluginName);
if (plugin == null || !plugin.IsLoaded)
{
return true;
}
object amountObj = amountType switch
{
AmountType.Double => (object)(double)amount,
AmountType.Float => (object)(float)amount,
AmountType.Int or _ => (object)(int)amount
};
object userObj = playerType switch
{
PlayerType.BasePlayer => player,
PlayerType.String => player.UserIDString,
PlayerType.ULong or _ => (ulong)player.userID,
};
double balance = Convert.ToDouble(plugin.Call(balanceHook, userObj));
if (string.IsNullOrEmpty(formattedCost))
{
formattedCost = amountObj.ToString();
}
else
{
formattedCost = formattedCost.Replace("{cost}", amountObj.ToString());
}
switch (balance >= amount)
{
case true:
plugin.Call(withdrawHook, userObj, amountObj);
Message(player, "Withdrawn", formattedCost);
return true;
case false:
Message(player, "NotWithdrawn", formattedCost);
return false;
}
}
[HookMethod("Teleport")]
public bool Teleport(BasePlayer player, Vector3 to, bool free = false)
{
if (!free && !config.Payments.All(m => TryPay(player, m)))
{
return false;
}
Vector3 from = player.transform.position;
try
{
player.UpdateActiveItem(default);
player.EnsureDismounted();
player.Server_CancelGesture();
if (player.HasParent())
{
player.SetParent(null, true, true);
}
if (player.IsConnected)
{
player.EndLooting();
StartSleeping(player);
}
player.RemoveFromTriggers();
player.Teleport(to);
if (player.IsConnected && !Net.sv.visibility.IsInside(player.net.group, to))
{
player.SetPlayerFlag(BasePlayer.PlayerFlags.ReceivingSnapshot, true);
player.ClientRPC(RpcTarget.Player("StartLoading", player));
player.SendEntityUpdate();
if (!player.limitNetworking)
{
player.UpdateNetworkGroup();
player.SendNetworkUpdateImmediate(false);
}
}
}
finally
{
if (!player.limitNetworking)
{
player.ForceUpdateTriggers();
}
}
Interface.CallHook("OnPlayerTeleported", player, from, to);
return true;
}
public void StartSleeping(BasePlayer player)
{
if (!player.IsSleeping())
{
Interface.CallHook("OnPlayerSleep", player);
player.SetPlayerFlag(BasePlayer.PlayerFlags.Sleeping, true);
player.sleepStartTime = Time.time;
BasePlayer.sleepingPlayerList.Add(player);
player.CancelInvoke("InventoryUpdate");
player.CancelInvoke("TeamUpdate");
}
}
[ChatCommand("return")]
private void returnCommand(BasePlayer player, string command, string[] args)
{
if (permission.UserHasPermission(player.UserIDString, TPPerm))
{
if (!ReturnLocations.TryGetValue(player.UserIDString, out var destination))
{
Message(player, "ReturnUnavailable");
return;
}
if (!CanPlayerTeleport(player, destination))
{
return;
}
if (!Teleport(player, destination, IsFree(player, args))) return;
Message(player, "ReturnUsed");
ReturnLocations.Remove(player.UserIDString);
}
else Message(player, "NotAllowed");
}
[ChatCommand("tpcorpse")]
private void tpCommand(BasePlayer player, string command, string[] args)
{
if (permission.UserHasPermission(player.UserIDString, AdminPerm))
{
if (args.Length == 0)
{
Message(player, "NeedTarget");
return;
}
var NameOrID = args[0] == "nocost" && args.Length > 1 ? args[1] : args[0];
if (ulong.TryParse(NameOrID, out ulong userid))
{
if (storedData.deaths.TryGetValue(NameOrID, out string value2))
{
Vector3 destination = value2.ToVector3();
if (!Teleport(player, destination, IsFree(player, args))) return;
var displayName = ConVar.Admin.GetPlayerName(userid);
Message(player, "ArrivedAtTheCorpse", displayName);
return;
}
}
var FoundPlayers = GetPlayers(NameOrID);
if (FoundPlayers.Count == 0)
{
Message(player, "InvalidPlayer", NameOrID);
return;
}
var target = FoundPlayers[0];
if (storedData.deaths.TryGetValue(target.UserIDString, out string value))
{
Vector3 destination = value.ToVector3();
if (!Teleport(player, destination, IsFree(player, args))) return;
Message(player, "ArrivedAtTheCorpse", target.displayName);
}
else Message(player, "UnknownLocationTarget", target.displayName);
}
else Message(player, "NotAllowed");
}
private bool IsFree(BasePlayer player, string[] args) => permission.UserHasPermission(player.UserIDString, NoCostPerm) || player.IsAdmin && config.nocost && args.Contains("nocost");
#endregion Commands
#region Config
protected override void LoadDefaultMessages()
{
var messages = new Dictionary<string, string>
{
["YouDied"] = "Your corpse was last seen {0} meters from here.",
["YouDiedGrid"] = "Your corpse was last seen {0} meters from here, in {1}.",
["TeleportingIn"] = "Teleporting to your corpse in {0} second(s).",
["TeleportBlockedCorpse"] = "Your corpse is in a restricted area, preventing teleportation.",
["TeleportBlockedPlayer"] = "You are not allowed to teleport from here.",
["TeleportBlockedTo"] = "You cannot teleport into a building blocked area.",
["TeleportBlockedFrom"] = "You cannot teleport from a building blocked area.",
["ArrivedAtYourCorpse"] = "You have arrived at your corpse.",
["ArrivedAtTheCorpse"] = "You have arrived at the corpse of {0}.",
["ReturnAvailable"] = "You can use <color=#ffa500ff>/return</color> to return to your initial location.",
["ReturnUnavailable"] = "You don't have a location set to return to.",
["ReturnUsed"] = "You have successfully returned to your initial location.",
["OutOfTeleports"] = "You have no more teleports left today.",
["TeleportsRemaining"] = "You have {0} teleports remaining today.",
["UnknownLocation"] = "Your last death location is unknown.",
["UnknownLocationTarget"] = "{0}'s last death location is unknown.",
["NeedTarget"] = "You need to specify a player to teleport to the corpse of, using either their name or steam id.",
["InvalidPlayer"] = "{0} is not part of a known player's name/id.",
["NotAllowed"] = "You do not have permission to use that command.",
["NotWithdrawn"] = "You do not have <color=#FFFF00>{0}</color> to pay for this corpse teleport!",
["Withdrawn"] = "You have paid <color=#FFFF00>{0}</color> for this corpse teleport!",
["DailyReset"] = "Daily teleports were reset."
};
lang.RegisterMessages(messages, this);
}
private void Message(BasePlayer player, string key, params object[] args)
{
if (!player.IsValid())
{
return;
}
string message = lang.GetMessage(key, this, player.UserIDString);
if (string.IsNullOrEmpty(message))
{
return;
}
Player.Message(player, args.Length == 0 ? message : string.Format(message, args), config.steamId);
}
private Configuration config;
public class Configuration
{
[JsonProperty(PropertyName = "Teleport payment methods", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<PaymentMethod> Payments;
[JsonProperty(PropertyName = "Show grid location")]
public bool showGrid = true;
[JsonProperty(PropertyName = "Track a corpse's location for x seconds")]
public int trackTime = 30;
[JsonProperty(PropertyName = "Allow teleporting to own corpse x times per day (0 for unlimited)")]
public int tpAmount = 5;
[JsonProperty(PropertyName = "Allow teleporting to own corpse x times per day (0 for unlimited), for VIPs")]
public int viptpAmount = 10;
[JsonProperty(PropertyName = "Allow returning to original location after teleporting")]
public bool allowReturn = false;
[JsonProperty(PropertyName = "Countdown until teleporting to own corpse (0 for instant tp)")]
public float tpCountdown = 5f;
[JsonProperty(PropertyName = "Block teleports into Zone Manager's tp blocked zones")]
public bool blockToZM = true;
[JsonProperty(PropertyName = "Block teleports from Zone Manager's tp blocked zones")]
public bool blockFromZM = true;
[JsonProperty(PropertyName = "Block teleports into building blocked areas")]
public bool blockToBuildBlocked = false;
[JsonProperty(PropertyName = "Block teleports from building blocked areas")]
public bool blockFromBuildBlocked = false;
[JsonProperty(PropertyName = "Ignore Abandoned Bases")]
public bool ignoreAB;
[JsonProperty(PropertyName = "Ignore Raidable Bases")]
public bool ignoreRB;
[JsonProperty(PropertyName = "Reset players' remaining teleports at this time (HH:mm:ss format)")]
public string resetTime = "00:00:00";
[JsonProperty(PropertyName = "Chat steam id")]
public ulong steamId;
[JsonProperty(PropertyName = "Allow admins to specify 'nocost' in commands")]
public bool nocost;
[JsonProperty(PropertyName = "ServerRewards Cost", NullValueHandling = NullValueHandling.Ignore)]
public int? SRC { get; set; } = null;
[JsonProperty(PropertyName = "Economics Cost", NullValueHandling = NullValueHandling.Ignore)]
public double? EC { get; set; } = null;
}
public class PaymentMethod
{
[JsonProperty(PropertyName = "Player Type (0 = BasePlayer, 1 = String, 2 = ULong)")]
public PlayerType PlayerType;
[JsonProperty(PropertyName = "Amount Type (0 = Double, 1 = Float, 2 = Int)")]
public AmountType AmountType;
public bool Enabled;
[JsonProperty(PropertyName = "Cost")]
public double Amount;
public string PluginName;
public string BalanceHook;
public string WithdrawHook;
public string CostFormat;
public PaymentMethod(bool enabled, PlayerType playerType, AmountType amountType, double amount, string pluginName, string balanceHook, string withdrawHook, string costFormat)
{
Enabled = enabled;
PlayerType = playerType;
AmountType = amountType;
Amount = amount;
PluginName = pluginName;
BalanceHook = balanceHook;
WithdrawHook = withdrawHook;
CostFormat = costFormat;
}
internal bool IsEnabled => Enabled && Amount > 0 && !string.IsNullOrEmpty(PluginName) && !string.IsNullOrEmpty(BalanceHook) && !string.IsNullOrEmpty(WithdrawHook);
}
protected override void LoadConfig()
{
base.LoadConfig();
canSaveConfig = false;
try
{
config = Config.ReadObject<Configuration>();
if (config == null) LoadDefaultConfig();
canSaveConfig = true;
}
catch (Exception ex)
{
Puts(ex.ToString());
LoadDefaultConfig();
}
if (config.Payments == null)
{
double economics = config.EC.HasValue ? config.EC.Value : 0;
int rewards = config.SRC.HasValue ? config.SRC.Value : 0;
config.Payments = new()
{
new(economics != 0, PlayerType.ULong, AmountType.Double, economics, "Economics", "Balance", "Withdraw", "${cost}"),
new(economics != 0, PlayerType.ULong, AmountType.Int, economics, "BankSystem", "Balance", "Withdraw", "${cost}"),
new(economics != 0, PlayerType.ULong, AmountType.Int, economics, "IQEconomic", "API_GET_BALANCE", "API_REMOVE_BALANCE", "${cost}"),
new(rewards != 0, PlayerType.ULong, AmountType.Int, rewards, "ServerRewards", "CheckPoints", "TakePoints", "{cost} RP"),
};
config.EC = null;
config.SRC = null;
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
config = new();
}
private bool canSaveConfig = true;
protected override void SaveConfig()
{
if (canSaveConfig)
{
Config.WriteObject(config);
}
}
#endregion
}
}