C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using ProtoBuf;
using UnityEngine;
namespace Oxide.Plugins {
[Info("TargetPractice", "k1lly0u", "0.2.2")]
class TargetPractice : RustPlugin {
#region Fields
private StatisticsData statistics;
private Hash<ulong, Queue<TargetHitInfo>> playerUI = new Hash<ulong, Queue<TargetHitInfo>>();
private readonly Vector3 localBullseye = new Vector3(-0.02269989f, 0.7262592f, 0.2032242f);
private const float RADIUS_BULLSEYE = 0.03f;
private const float RADIUS_INNER_1 = 0.11f;
private const float RADIUS_INNER_2 = 0.19f;
private const float RADIUS_INNER_3 = 0.29f;
private const float RADIUS_OUTER = 0.36f;
private const uint TARGET_BONE_ID = 365102685;
private const uint BULLSEYE_BONE_ID = 2528224745;
private const string POPUP_ELEMENT = "tpui.popup.{0}";
private const string POPUP_FORMAT = "{0}pts @ {1}m";
private const string ADMIN_PERMISSION = "targetpractice.admin";
private enum Hit {
Bullseye, Yellow, Red, Blue, Edge
}
private static string[] HitColors;
private FieldInfo _knockdownHealth = typeof(ReactiveTarget).GetField("knockdownHealth", BindingFlags.Public | BindingFlags.NonPublic);
private FieldInfo _lastToggleTime = typeof(ReactiveTarget).GetField("lastToggleTime", BindingFlags.Public | BindingFlags.NonPublic);
#endregion
#region Oxide Hooks
private void Loaded() {
lang.RegisterMessages(Messages, this);
Unsubscribe(nameof(OnEntitySpawned));
permission.RegisterPermission(ADMIN_PERMISSION, this);
HitColors = new string[5];
HitColors[0] = UI.Color("95c055", 0.95f);
HitColors[1] = UI.Color("dac15f", 0.95f);
HitColors[2] = UI.Color("cb3427", 0.95f);
HitColors[3] = UI.Color("30778a", 0.95f);
HitColors[4] = UI.Color("72544b", 0.95f);
}
private void OnServerInitialized() {
LoadData();
if (Configuration.Target.KnockdownHealth != 100f) {
Subscribe(nameof(OnEntitySpawned));
foreach (BaseNetworkable entity in BaseNetworkable.serverEntities) {
if (entity is ReactiveTarget) OnEntitySpawned(entity as ReactiveTarget);
}
}
}
private void OnServerSave() => SaveData();
private void OnEntitySpawned(ReactiveTarget reactiveTarget) => _knockdownHealth.SetValue(reactiveTarget, Configuration.Target.KnockdownHealth);
private void OnEntityTakeDamage(ReactiveTarget reactiveTarget, HitInfo info) {
if (info.HitBone != TARGET_BONE_ID && info.HitBone != BULLSEYE_BONE_ID) return;
if (info.InitiatorPlayer == null) return;
BasePlayer player = info.InitiatorPlayer;
Item weapon = player.GetActiveItem();
if (weapon == null || !Configuration.Weapons.Contains(weapon.info.shortname)) return;
Hit hit;
float score = 0;
if (info.HitBone.Equals(BULLSEYE_BONE_ID)) {
hit = Hit.Bullseye;
score = Configuration.Scores[hit];
}
else {
Vector3 bullseyeWorld = reactiveTarget.transform.TransformPoint(localBullseye);
score = EvaluateScore(Vector3.Distance(bullseyeWorld, info.HitPositionWorld), out hit);
}
if (Configuration.Target.ResetTime != 6f) {
float knockdownHealth = (float)_knockdownHealth.GetValue(reactiveTarget);
if (knockdownHealth <= 0f) {
reactiveTarget.CancelInvoke(reactiveTarget.ResetTarget);
reactiveTarget.Invoke(() => ResetTarget(reactiveTarget), Configuration.Target.ResetTime);
}
}
double d = Math.Round(Vector3Ex.Distance2D(player.transform.position, reactiveTarget.transform.position), 2, MidpointRounding.AwayFromZero);
TargetHitInfo targetHitInfo = new TargetHitInfo(player.userID, hit, score, d, FormatWeaponName(weapon));
GetUIComponent(player).Enqueue(targetHitInfo);
if (statistics.AddScore(player, targetHitInfo) && Configuration.Notify) BroadcastTopScore(player.displayName, targetHitInfo);
}
private void Unload() {
foreach (PlayerInterface ui in UnityEngine.Object.FindObjectsOfType<PlayerInterface>()) UnityEngine.Object.Destroy(ui);
HitColors = null;
Configuration = null;
}
#endregion
#region Functions
private float EvaluateScore(float f, out Hit hit) {
if (f < RADIUS_BULLSEYE) hit = Hit.Bullseye;
else if (f < RADIUS_INNER_1) hit = Hit.Yellow;
else if (f < RADIUS_INNER_2) hit = Hit.Red;
else if (f < RADIUS_INNER_3) hit = Hit.Blue;
else hit = Hit.Edge;
return Configuration.Scores[hit];
}
private void ResetTarget(ReactiveTarget reactiveTarget) {
if (!reactiveTarget.IsKnockedDown() || !reactiveTarget.CanToggle()) return;
_lastToggleTime.SetValue(reactiveTarget, UnityEngine.Time.realtimeSinceStartup);
reactiveTarget.SetFlag(BaseEntity.Flags.On, true, false, true);
_knockdownHealth.SetValue(reactiveTarget, Configuration.Target.KnockdownHealth);
reactiveTarget.MarkDirtyForceUpdateOutputs();
}
private string FormatWeaponName(Item item) {
string str = item.info.displayName.english;
if (item.contents?.itemList?.Count > 0) str += string.Format(" ({0})", item.contents?.itemList.Select(x => x.info.displayName.english).ToSentence());
return str;
}
private void ChatMessage(BasePlayer player, string key, params object[] args) {
if (args?.Length == 0) player.ChatMessage(Msg(key, player.UserIDString));
else player.ChatMessage(string.Format(Msg(key, player.UserIDString), args));
}
private void BroadcastTopScore(string playerName, TargetHitInfo targetHitInfo) {
foreach (BasePlayer player in BasePlayer.activePlayerList) {
string score = string.Format(Msg("Notification.ServerScoreFormat", player.UserIDString), player.displayName, targetHitInfo.distance, (Hit)targetHitInfo.hit, targetHitInfo.score, targetHitInfo.weapon);
ChatMessage(player, "Notification.NewTopScore", score);
}
}
#endregion
#region Player UI Component
private PlayerInterface GetUIComponent(BasePlayer player) => player.GetComponent<PlayerInterface>() ?? player.gameObject.AddComponent<PlayerInterface>();
private class PlayerInterface : MonoBehaviour {
internal BasePlayer Player {
get;
private set;
}
internal List<TimedPopup> queue = Facepunch.Pool.GetList<TimedPopup>();
internal List<string> elements = Facepunch.Pool.GetList<string>();
private void Awake() {
Player = GetComponent<BasePlayer>();
}
private void OnDestroy() {
elements.ForEach(x => CuiHelper.DestroyUi(Player, x));
Facepunch.Pool.FreeList(ref queue);
Facepunch.Pool.FreeList(ref elements);
}
internal void Enqueue(TargetHitInfo targetHitInfo) {
queue.Add(new TimedPopup(UnityEngine.Time.time + Configuration.Popup.Lifetime, targetHitInfo));
if (elements.Count < Configuration.Popup.Limit) {
string element = CreatePopup(Player, targetHitInfo, elements.Count);
elements.Add(element);
if (!InvokeHandler.IsInvoking(Player, TimedRefresh)) InvokeHandler.Invoke(Player, TimedRefresh, Configuration.Popup.Lifetime);
}
}
private void TimedRefresh() {
elements.ForEach(x => CuiHelper.DestroyUi(Player, x));
elements.Clear();
float time = UnityEngine.Time.time;
for (int i = queue.Count - 1; i >= 0; i--) {
if (time >= queue[i].expiry) queue.RemoveAt(i);
}
if (queue.Count == 0) {
Destroy(this);
return;
}
float nextRefreshTime = float.PositiveInfinity;
for (int i = 0; i < Mathf.Min(queue.Count, Configuration.Popup.Limit);
i++) {
TimedPopup timedPopup = queue[i];
if (timedPopup.expiry < nextRefreshTime) nextRefreshTime = timedPopup.expiry - time;
string element = CreatePopup(Player, timedPopup.targetHitInfo, i);
elements.Add(element);
}
InvokeHandler.Invoke(Player, TimedRefresh, nextRefreshTime);
}
internal struct TimedPopup {
internal float expiry;
internal TargetHitInfo targetHitInfo;
internal TimedPopup(float expiry, TargetHitInfo targetHitInfo) {
this.expiry = expiry;
this.targetHitInfo = targetHitInfo;
}
}
}
#endregion
#region UI
public static class UI {
public static CuiElementContainer Container(string panelName, string color, UI4 dimensions, bool useCursor = false, string parent = "Overlay") {
CuiElementContainer container = new CuiElementContainer() {
{
new CuiPanel {
Image = {
Color = color, Material = "assets/content/ui/uibackgroundblur-ingamemenu.mat"
}
, RectTransform = {
AnchorMin = dimensions.GetMin(), AnchorMax = dimensions.GetMax()
}
, CursorEnabled = useCursor
}
, new CuiElement().Parent = parent, panelName.ToString()
}
}
;
return container;
}
public static void Image(ref CuiElementContainer container, string panel, string url, UI4 dimensions) {
container.Add(new CuiElement {
Name = CuiHelper.GetGuid(), Parent = panel, Components = {
new CuiRawImageComponent {
Url = url
}
, new CuiRectTransformComponent {
AnchorMin = dimensions.GetMin(), AnchorMax = dimensions.GetMax()
}
}
}
);
}
public static void Panel(ref CuiElementContainer container, string panel, string color, UI4 dimensions) {
container.Add(new CuiPanel {
Image = {
Color = color
}
, RectTransform = {
AnchorMin = dimensions.GetMin(), AnchorMax = dimensions.GetMax()
}
, CursorEnabled = false
}
, panel.ToString());
}
public static void Label(ref CuiElementContainer container, string panel, string text, int size, UI4 dimensions, TextAnchor align = TextAnchor.MiddleCenter) {
container.Add(new CuiLabel {
Text = {
FontSize = size, Align = align, Text = text, Font = "robotocondensed-regular.ttf"
}
, RectTransform = {
AnchorMin = dimensions.GetMin(), AnchorMax = dimensions.GetMax()
}
}
, panel.ToString());
}
public static string Color(string hexColor, float alpha) {
if (hexColor.StartsWith("#")) hexColor = hexColor.Substring(1);
int red = int.Parse(hexColor.Substring(0, 2), NumberStyles.AllowHexSpecifier);
int green = int.Parse(hexColor.Substring(2, 2), NumberStyles.AllowHexSpecifier);
int blue = int.Parse(hexColor.Substring(4, 2), NumberStyles.AllowHexSpecifier);
return $"{(double)red / 255} {(double)green / 255} {(double)blue / 255} {alpha}";
}
}
public class UI4 {
public float xMin, yMin, xMax, yMax;
public UI4(float xMin, float yMin, float xMax, float yMax) {
this.xMin = xMin;
this.yMin = yMin;
this.xMax = xMax;
this.yMax = yMax;
}
public string GetMin() => $"{xMin} {yMin}";
public string GetMax() => $"{xMax} {yMax}";
}
#endregion
#region UI Creation
private static readonly UI4 IconPosition = new UI4(0f, 0f, 0.1f, 1f);
private static readonly UI4 TextPosition = new UI4(0.15f, 0f, 1f, 1f);
private static string CreatePopup(BasePlayer player, TargetHitInfo info, int position) {
string panel = string.Format(POPUP_ELEMENT, position);
CuiElementContainer container = UI.Container(panel, HitColors[0], CalculateAnchor(position));
if (info.IsBullseye()) UI.Image(ref container, panel, Configuration.Bullseye, IconPosition);
else UI.Panel(ref container, panel, HitColors[(int)info.hit], IconPosition);
UI.Label(ref container, panel, string.Format(POPUP_FORMAT, info.score, info.distance), 12, TextPosition, TextAnchor.MiddleLeft);
CuiHelper.AddUi(player, container);
return panel;
}
private static UI4 CalculateAnchor(int index, float xMin = 0.839f, float yMin = 0.208f, float xDim = 0.148f, float yDim = 0.027f, float spacing = 0.01f) {
float y = yMin + ((yDim + spacing) * index);
return new UI4(xMin, y, xMin + xDim, y + yDim);
}
[ProtoContract]
private struct TargetHitInfo {
[ProtoMember(1)]
public string userId;
[ProtoMember(2)]
public int hit;
[ProtoMember(3)]
public float score;
[ProtoMember(4)]
public double distance;
[ProtoMember(5)]
public string weapon;
[ProtoMember(6)]
public double rangedScore;
public TargetHitInfo(ulong userId, Hit hit, float score, double distance, string weapon) {
this.userId = userId.ToString();
this.hit = (int)hit;
this.score = score;
this.distance = distance;
this.weapon = weapon;
rangedScore = distance * Configuration.Scores[hit];
}
internal bool IsBullseye() => (Hit)hit == Hit.Bullseye;
}
#endregion
#region Commands
[ChatCommand("target")]
private void cmdTarget(BasePlayer player, string command, string[] args) {
if (args.Length == 0) {
ChatMessage(player, "Message.Prefix");
ChatMessage(player, "Chat.Help1");
ChatMessage(player, "Chat.Help2");
ChatMessage(player, "Chat.Help4");
if (permission.UserHasPermission(player.UserIDString, ADMIN_PERMISSION)) ChatMessage(player, "Chat.Help3");
return;
}
switch (args[0].ToLower()) {
case "top" : {
List<TargetHitInfo> list = statistics._sortedScores;
if (list.Count > 0) {
string scores = string.Empty;
for (int i = 0; i < Mathf.Min(10, list.Count);
i++) {
TargetHitInfo targetHitInfo = list[i];
scores += string.Format(Msg("Notification.ServerScoreFormat", player.UserIDString), covalence.Players.FindPlayerById(targetHitInfo.userId).Name ?? targetHitInfo.userId, targetHitInfo.distance, (Hit)targetHitInfo.hit, targetHitInfo.score, targetHitInfo.weapon);
}
ChatMessage(player, "Notification.TopServer", scores);
}
else ChatMessage(player, "Notification.NoScores");
}
return;
case "pb" : {
List<TargetHitInfo> list;
if (statistics.GetUserTopHits(player.userID, out list)) {
string scores = string.Empty;
for (int i = 0; i < Mathf.Min(10, list.Count);
i++) {
TargetHitInfo targetHitInfo = list[i];
scores += string.Format(Msg("Notification.PBScoreFormat", player.UserIDString), targetHitInfo.distance, (Hit)targetHitInfo.hit, targetHitInfo.score, targetHitInfo.weapon);
}
ChatMessage(player, "Notification.TopPB", scores);
}
else ChatMessage(player, "Notification.NoScores");
}
return;
case "scores" : {
ulong userId = 0UL;
string displayName = string.Empty;
if (args.Length > 1) {
Core.Libraries.Covalence.IPlayer iPlayer = covalence.Players.FindPlayer(args[1]);
if (iPlayer != null) {
Debug.Log("found player");
userId = ulong.Parse(iPlayer.Id);
displayName = iPlayer.Name;
}
}
else {
userId = player.userID;
displayName = player.displayName;
}
if (userId == 0UL) {
ChatMessage(player, "Notification.NoUserFound", args[1]);
return;
}
string scoreStr = Msg("Notification.StatsFormat", player.UserIDString);
if (statistics.FindUserScores(userId, ref scoreStr)) ChatMessage(player, "Notification.StatsForUser", displayName, scoreStr);
else ChatMessage(player, "Notification.NoDataFound", displayName);
}
return;
case "wipe" : if (!permission.UserHasPermission(player.UserIDString, ADMIN_PERMISSION)) return;
statistics._players.Clear();
statistics._sortedScores.Clear();
SaveData();
ChatMessage(player, "Notification.WipedScores");
return;
default : ChatMessage(player, "Notification.InvalidSyntax");
break;
}
}
#endregion
#region Config
private static ConfigData Configuration;
private class ConfigData {
[JsonProperty(PropertyName = "Score Values")]
public Hash<Hit, float> Scores {
get;
set;
}
[JsonProperty(PropertyName = "Popup Options")]
public PopupOptions Popup {
get;
set;
}
[JsonProperty(PropertyName = "Target Options")]
public TargetOptions Target {
get;
set;
}
[JsonProperty(PropertyName = "Send server wide notification if the top server score has been beaten")]
public bool Notify {
get;
set;
}
[JsonProperty(PropertyName = "Weapons allowed to be used for scoring")]
public string[] Weapons {
get;
set;
}
[JsonProperty(PropertyName = "Bullseye image URL")]
public string Bullseye {
get;
set;
}
public class TargetOptions {
[JsonProperty(PropertyName = "Amount of damage to take to be knocked down")]
public float KnockdownHealth {
get;
set;
}
[JsonProperty(PropertyName = "Amount of time it takes to reset the target")]
public float ResetTime {
get;
set;
}
}
public class PopupOptions {
[JsonProperty(PropertyName = "Popup lifetime (seconds)")]
public int Lifetime {
get;
set;
}
[JsonProperty(PropertyName = "Maximum active popup elements")]
public int Limit {
get;
set;
}
}
public class ScoreValues {
public int Bullseye {
get;
set;
}
public int Yellow {
get;
set;
}
public int Red {
get;
set;
}
public int Blue {
get;
set;
}
public int Edge {
get;
set;
}
}
public Oxide.Core.VersionNumber Version {
get;
set;
}
}
protected override void LoadConfig() {
base.LoadConfig();
Configuration = Config.ReadObject<ConfigData>();
if (Configuration.Version < Version) UpdateConfigValues();
Config.WriteObject(Configuration, true);
}
protected override void LoadDefaultConfig() => Configuration = GetBaseConfig();
private ConfigData GetBaseConfig() {
return new ConfigData {
Notify = true, Popup = new ConfigData.PopupOptions {
Lifetime = 5, Limit = 15,
}
, Scores = new Hash<Hit, float> {
[Hit.Bullseye]
= 5f, [Hit.Yellow] = 4f, [Hit.Red] = 3f, [Hit.Blue] = 2f, [Hit.Edge] = 1f,
}
, Target = new ConfigData.TargetOptions {
KnockdownHealth = 100f, ResetTime = 6f
}
, Weapons = new string[] {
"rifle.ak", "rifle.bolt", "rifle.l96", "rifle.lr300", "rifle.m39", "rifle.semiauto", "lmg.m249", "bow.compound", "bow.hunting", "crossbow", "smg.2", "smg.thompson", "smg.mp5", "pistol.semiauto", "pistol.revolver", "pistol.m92", "pistol.pythn", "pistol.nailgun",
}
, Bullseye = "https://www.chaoscode.io/oxide/Images/bullseye.png", Version = Version
}
;
}
protected override void SaveConfig() => Config.WriteObject(Configuration, true);
private void UpdateConfigValues() {
PrintWarning("Config update detected! Updating config values...");
ConfigData baseConfig = GetBaseConfig();
if (Configuration.Version < new VersionNumber(0, 2, 0)) Configuration = baseConfig;
Configuration.Version = Version;
PrintWarning("Config update completed!");
}
#endregion
#region Data Management
private void SaveData() => ProtoStorage.Save<StatisticsData>(statistics, Title);
private void LoadData() {
statistics = ProtoStorage.Load<StatisticsData>(Title);
if (statistics == null || statistics._sortedScores == null || statistics._players == null) statistics = new StatisticsData();
}
[ProtoContract]
private class StatisticsData {
[ProtoMember(1)]
public Hash<ulong, List<TargetHitInfo>> _players = new Hash<ulong, List<TargetHitInfo>>();
[ProtoMember(2)]
public List<TargetHitInfo> _sortedScores = new List<TargetHitInfo>();
[JsonIgnore]
private CompareScore comparer;
public StatisticsData() {
comparer = new CompareScore();
}
public bool AddScore(BasePlayer player, TargetHitInfo targetHitInfo) {
List<TargetHitInfo> list;
if (!_players.TryGetValue(player.userID, out list)) list = _players[player.userID] = new List<TargetHitInfo>();
int search = list.BinarySearch(targetHitInfo, (IComparer<TargetHitInfo>)comparer);
if (search < 0) list.Insert(~search, targetHitInfo);
search = _sortedScores.BinarySearch(targetHitInfo, (IComparer<TargetHitInfo>)comparer);
if (search < 0) _sortedScores.Insert(~search, targetHitInfo);
if (~search == 0) return true;
return false;
}
public bool GetUserTopHits(ulong playerId, out List<TargetHitInfo> list) => _players.TryGetValue(playerId, out list);
public bool FindUserScores(ulong playerId, ref string format) {
List<TargetHitInfo> list;
if (!_players.TryGetValue(playerId, out list)) return false;
float totalScore = list.Sum(x => x.score);
format = string.Format(format, string.Format(POPUP_FORMAT, list[0].score, list[0].distance), totalScore, list.Count, (float)list.Sum(x => x.distance) / list.Count, totalScore / list.Count);
return true;
}
private class CompareScore : IComparer<TargetHitInfo> {
public int Compare(TargetHitInfo a, TargetHitInfo b) {
if (TargetHitInfo.ReferenceEquals(a, b)) return 0;
else return a.rangedScore.CompareTo(b.rangedScore) * -1;
}
}
}
#endregion
#region Localization
private string Msg(string key, string playerId = null) => lang.GetMessage(key, this, playerId);
private readonly Dictionary<string, string> Messages = new Dictionary<string, string> {
["Message.Prefix"]
= "<color=#ce422b>[ Target Practice ]</color> ", ["Chat.Help1"] = "<color=#ce422b>/target top</color> - Display the top 10 scores on the server", ["Chat.Help2"] = "<color=#ce422b>/target pb</color> - Display your personal top 10 scores", ["Chat.Help3"] = "<color=#ce422b>/target wipe</color> - Wipes all score data", ["Chat.Help4"] = "<color=#ce422b>/target scores <opt:playername></color> - Check your stats/score, or enter a players name to view their stats/score", ["Notification.InvalidSyntax"] = "Invalid syntax! Type <color=#ce422b>/target</color> for help", ["Notification.WipedScores"] = "All scores have been wiped", ["Notification.NoScores"] = "You do not have any scores saved", ["Notification.TopPB"] = "<color=#ce422b>[ Target Practice ]</color> - Personal Best\n{0}", ["Notification.PBScoreFormat"] = "\nDistance: <color=#ce422b>{0}</color> | Hit: <color=#ce422b>{1}</color> | Score: <color=#ce422b>{2}</color>\nWeapon: <color=#ce422b>{3}</color>\n", ["Notification.TopServer"] = "<color=#ce422b>[ Target Practice ]</color> - Server Best\n{0}", ["Notification.ServerScoreFormat"] = "\n<color=#ce422b>{0}</color> | Distance: <color=#ce422b>{1}</color> | Hit: <color=#ce422b>{2}</color>\nScore: <color=#ce422b>{3}</color> | Weapon: <color=#ce422b>{4}</color>\n", ["Notification.NewTopScore"] = "<color=#ce422b>[ Target Practice ]</color> - A new top score has been set!\n{0}", ["Notification.NoUserFound"] = "Unable to find player with partial name: <color=#ce422b>{0}</color>", ["Notification.NoDataFound"] = "No data saved for player: <color=#ce422b>{0}</color>", ["Notification.StatsForUser"] = "<color=#ce422b>[ Target Practice ]</color> - Stats for <color=#ce422b>{0}</color>\n{1}", ["Notification.StatsFormat"] = "\nBest Score: <color=#ce422b>{0}</color>\nTotal Score: <color=#ce422b>{1}</color>\nTotal Hits: <color=#ce422b>{2}</color>\nAvg Distance: <color=#ce422b>{3}</color>\nAvg Score: <color=#ce422b>{4}</color>\n",
}
;
#endregion
}
}