C#:
// Reference: System.Drawing
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Plugins.SignArtistClasses;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;
using UnityEngine.Networking;
using Color = System.Drawing.Color;
using Graphics = System.Drawing.Graphics;
using Steamworks;
namespace Oxide.Plugins
{
[Info("Sign Artist", "Whispers88", "1.4.6")]
[Description("Allows players with the appropriate permission to import images from the internet on paintable objects")]
/*********************************************************************************
* This plugin was originally created by Bombardir and then maintained by Nogrod.
* It was rewritten from scratch by Mughisi on January 12th, 2018.
*********************************************************************************/
internal class SignArtist : RustPlugin
{
private Dictionary<ulong, float> cooldowns = new Dictionary<ulong, float>();
private GameObject imageDownloaderGameObject;
private ImageDownloader imageDownloader;
SignArtistConfig Settings { get; set; }
Dictionary<string, ImageSize> ImageSizePerAsset { get; set; }
Dictionary<ulong, string> SkiniconUrls = new Dictionary<ulong, string>();
private const string ItemIconUrl = "https://www.rustedit.io/images/imagelibrary/{0}.png";
/// <summary>
/// Plugin configuration
/// </summary>
public class SignArtistConfig
{
[JsonProperty(PropertyName = "Time in seconds between download requests (0 to disable)")]
public int Cooldown { get; set; }
[JsonProperty(PropertyName = "Maximum concurrent downloads")]
public int MaxActiveDownloads { get; set; }
[JsonProperty(PropertyName = "Maximum distance from the sign")]
public int MaxDistance { get; set; }
[JsonProperty(PropertyName = "Maximum filesize in MB")]
public float MaxSize { get; set; }
[JsonProperty(PropertyName = "Enforce JPG file format")]
public bool EnforceJpeg { get; set; }
[JsonProperty(PropertyName = "Enable Header Check")]
public bool HeaderCheck { get; set; }
[JsonProperty(PropertyName = "Enforce Header Check")]
public bool HeaderCheckEnforce { get; set; }
[JsonProperty(PropertyName = "JPG image quality (0-100)")]
public int Quality
{
get
{
return quality;
}
set
{
// Validate the value, it can't be less than 0 and not more than 100.
if (value >= 0 && value <= 100)
{
quality = value;
}
else
{
// Set the quality to a default value of 85% when an invalid value was specified.
quality = value > 100 ? 100 : 85;
}
}
}
[JsonProperty("Enable logging file")]
public bool FileLogging { get; set; }
[JsonProperty("Enable logging console")]
public bool ConsoleLogging { get; set; }
[JsonProperty("Enable discord logging")]
public bool Discordlogging { get; set; }
[JsonProperty("Discord Webhook")]
public string DiscordWebhook { get; set; }
[JsonProperty("Avatar URL")]
public string AvatarUrl { get; set; }
[JsonProperty("Discord Username")]
public string DiscordUsername { get; set; }
[JsonIgnore]
public float MaxFileSizeInBytes
{
get
{
return MaxSize * 1024 * 1024;
}
}
private int quality = 85;
/// <summary>
/// Creates a default configuration file
/// </summary>
/// <returns>Default config</returns>
public static SignArtistConfig DefaultConfig()
{
return new SignArtistConfig
{
Cooldown = 0,
MaxSize = 1,
MaxDistance = 3,
MaxActiveDownloads = 5,
EnforceJpeg = false,
HeaderCheck = true,
HeaderCheckEnforce = false,
Quality = 85,
FileLogging = false,
ConsoleLogging = false,
Discordlogging = false,
DiscordWebhook = "",
AvatarUrl = "https://i.imgur.com/dH7V1Dh.png",
DiscordUsername = "Sign Artist"
};
}
}
/// <summary>
/// A type used to request new images to download.
/// </summary>
private class DownloadRequest
{
public BasePlayer Sender { get; }
public IPaintableEntity Sign { get; }
public string Url { get; set; }
public bool Raw { get; }
public bool Hor { get; }
public uint TextureIndex { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DownloadRequest" ></see> class.
/// </summary>
/// <param name="url">The URL to download the image from. </param>
/// <param name="player">The player that requested the download. </param>
/// <param name="sign">The sign to add the image to. </param>
/// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
public DownloadRequest(string url, BasePlayer player, IPaintableEntity sign, bool raw, bool hor, uint textureIndex)
{
Url = url;
Sender = player;
Sign = sign;
Raw = raw;
Hor = hor;
TextureIndex = textureIndex;
}
}
/// <summary>
/// A type used to request new images to be restored.
/// </summary>
private class RestoreRequest
{
public BasePlayer Sender { get; }
public IPaintableEntity Sign { get; }
public bool Raw { get; }
public uint TextureIndex { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RestoreRequest" ></see> class.
/// </summary>
/// <param name="player">The player that requested the restore. </param>
/// <param name="sign">The sign to restore the image from. </param>
/// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
public RestoreRequest(BasePlayer player, IPaintableEntity sign, bool raw, uint textureIndex)
{
Sender = player;
Sign = sign;
Raw = raw;
TextureIndex = textureIndex;
}
}
/// <summary>
/// A type used to determine the size of the image for a sign
/// </summary>
public class ImageSize
{
public int Width { get; }
public int Height { get; }
public int ImageWidth { get; }
public int ImageHeight { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ImageSize" ></see> class.
/// </summary>
/// <param name="width">The width of the canvas and the image. </param>
/// <param name="height">The height of the canvas and the image. </param>
public ImageSize(int width, int height) : this(width, height, width, height)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageSize" ></see> class.
/// </summary>
/// <param name="width">The width of the canvas. </param>
/// <param name="height">The height of the canvas. </param>
/// <param name="imageWidth">The width of the image. </param>
/// <param name="imageHeight">The height of the image. </param>
public ImageSize(int width, int height, int imageWidth, int imageHeight)
{
Width = width;
Height = height;
ImageWidth = imageWidth;
ImageHeight = imageHeight;
}
}
#region Image Download Behaviour
/// <summary>
/// UnityEngine script to be attached to a GameObject to download images and apply them to signs.
/// </summary>
private class ImageDownloader : MonoBehaviour
{
private byte activeDownloads;
private byte activeRestores;
private readonly SignArtist signArtist = (SignArtist)Interface.Oxide.RootPluginManager.GetPlugin(nameof(SignArtist));
private readonly Queue<DownloadRequest> downloadQueue = new Queue<DownloadRequest>();
private readonly Queue<RestoreRequest> restoreQueue = new Queue<RestoreRequest>();
/// <summary>
/// Queue a new image to download and add to a sign
/// </summary>
/// <param name="url">The URL to download the image from. </param>
/// <param name="player">The player that requested the download. </param>
/// <param name="sign">The sign to add the image to. </param>
/// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
public void QueueDownload(string url, BasePlayer player, IPaintableEntity sign, uint textureIndex, bool raw, bool hor = false)
{
// Check if there is already a request for this sign and show an error if there is.
bool existingRequest = downloadQueue.Any(request => request.Sign == sign) || restoreQueue.Any(request => request.Sign == sign);
if (existingRequest)
{
signArtist.SendMessage(player, "ActionQueuedAlready");
return;
}
// Instantiate a new DownloadRequest and add it to the queue.
downloadQueue.Enqueue(new DownloadRequest(url, player, sign, raw, hor, textureIndex));
// Attempt to start the next download.
StartNextDownload();
}
/// <summary>
/// Attempts to restore a sign.
/// </summary>
/// <param name="player">The player that requested the restore. </param>
/// <param name="sign">The sign to restore the image from. </param>
/// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
public void QueueRestore(BasePlayer player, IPaintableEntity sign, bool raw, uint textureIndex)
{
// Check if there is already a request for this sign and show an error if there is.
bool existingRequest = downloadQueue.Any(request => request.Sign == sign) || restoreQueue.Any(request => request.Sign == sign);
if (existingRequest)
{
signArtist.SendMessage(player, "ActionQueuedAlready");
return;
}
// Instantiate a new RestoreRequest and add it to the queue.
restoreQueue.Enqueue(new RestoreRequest(player, sign, raw, textureIndex));
// Attempt to start the next restore.
StartNextRestore();
}
/// <summary>
/// Starts the next download if available.
/// </summary>
/// <param name="reduceCount"></param>
private void StartNextDownload(bool reduceCount = false)
{
// Check if we need to reduce the active downloads counter after a succesful or failed download.
if (reduceCount)
{
activeDownloads--;
}
// Check if we don't have the maximum configured amount of downloads running already.
if (activeDownloads >= signArtist.Settings.MaxActiveDownloads)
{
return;
}
// Check if there is still an image in the queue.
if (downloadQueue.Count <= 0)
{
return;
}
// Increment the active downloads by 1 and start the download process.
activeDownloads++;
StartCoroutine(DownloadImage(downloadQueue.Dequeue()));
}
/// <summary>
/// Starts the next restore if available.
/// </summary>
/// <param name="reduceCount"></param>
private void StartNextRestore(bool reduceCount = false)
{
// Check if we need to reduce the active restores counter after a succesful or failed restore.
if (reduceCount)
{
activeRestores--;
}
// Check if we don't have the maximum configured amount of restores running already.
if (activeRestores >= signArtist.Settings.MaxActiveDownloads)
{
return;
}
// Check if there is still an image in the queue.
if (restoreQueue.Count <= 0)
{
return;
}
// Increment the active restores by 1 and start the restore process.
activeRestores++;
StartCoroutine(RestoreImage(restoreQueue.Dequeue()));
}
/// <summary>
/// Downloads the image and adds it to the sign.
/// </summary>
/// <param name="request">The requested <see cref="DownloadRequest"></see> instance. </param>
///
public class SizeLimitingHandler : DownloadHandlerScript
{
private int maxSizeBytes;
private int receivedBytes = 0;
private List<byte> data = new List<byte>();
public bool Aborted { get; private set; } = false;
public SizeLimitingHandler(int maxSizeBytes) : base(new byte[1024])
{
this.maxSizeBytes = maxSizeBytes;
}
protected override bool ReceiveData(byte[] buffer, int dataLength)
{
receivedBytes += dataLength;
if (receivedBytes > maxSizeBytes)
{
Aborted = true;
return false;
}
data.AddRange(buffer.Take(dataLength));
return true;
}
public byte[] GetData() => data.ToArray();
}
private IEnumerator DownloadImage(DownloadRequest request)
{
if (ItemManager.itemDictionaryByName.ContainsKey(request.Url))
{
request.Url = string.Format(ItemIconUrl, request.Url);
}
if (signArtist.Settings.HeaderCheck)
{
UnityWebRequest head = UnityWebRequest.Head(request.Url);
head.timeout = 10;
yield return head.SendWebRequest();
if (head.result != UnityWebRequest.Result.Success)
{
signArtist.PrintWarning(head.error + "Cannot get headers from:" + request.Url);
head.Dispose();
StartNextDownload(true);
if (signArtist.Settings.HeaderCheckEnforce)
yield break;
}
string contentlength = head.GetResponseHeader("Content-Length");
if (string.IsNullOrEmpty(contentlength))
{
head.Dispose();
signArtist.PrintWarning("Could not get content length from:" + request.Url);
StartNextDownload(true);
if (signArtist.Settings.HeaderCheckEnforce)
yield break;
}
int filesize = int.Parse(contentlength);
if (filesize > signArtist.Settings.MaxFileSizeInBytes)
{
head.Dispose();
signArtist.PrintWarning($"Filesize is {filesize / 1000000} MB the maximum size allowed is {signArtist.Settings.MaxSize} MB");
signArtist.SendMessage(request.Sender, "FileTooLarge", signArtist.Settings.MaxSize);
StartNextDownload(true);
yield break;
}
head.Dispose();
}
var handler = new SizeLimitingHandler((int)signArtist.Settings.MaxFileSizeInBytes);
UnityWebRequest www = new UnityWebRequest(request.Url, UnityWebRequest.kHttpVerbGET, handler, null);
www.timeout = 10;
yield return www.SendWebRequest();
// Verify that there is a valid reference to the plugin from this class.
if (signArtist == null)
{
throw new NullReferenceException("signArtist");
}
// Verify that the webrequest was succesful.
if (www.result != UnityWebRequest.Result.Success)
{
// The webrequest wasn't succesful, show a message to the player and attempt to start the next download.
signArtist.SendMessage(request.Sender, "WebErrorOccurred", www.error);
www.Dispose();
StartNextDownload(true);
yield break;
}
var data = handler.GetData();
if (handler.Aborted || data.Length > signArtist.Settings.MaxFileSizeInBytes)
{
// The file is too large, show a message to the player and attempt to start the next download.
signArtist.SendMessage(request.Sender, "FileTooLarge", signArtist.Settings.MaxSize);
www.Dispose();
StartNextDownload(true);
yield break;
}
// Get the bytes array for the image from the webrequest and lookup the target image size for the targeted sign.
byte[] imageBytes;
if (request.Raw)
{
imageBytes = data;
}
else
{
imageBytes = GetImageBytes(data);
}
ImageSize size = GetImageSizeFor(request.Sign);
// Verify that we have image size data for the targeted sign.
if (size == null)
{
signArtist.SendMessage(request.Sender, "ErrorOccurred");
signArtist.PrintWarning($"Couldn't find the required image size for {request.Sign.PrefabName}, please report this in the plugin's thread.");
StartNextDownload(true);
www.Dispose();
yield break;
}
RotateFlipType rotation = RotateFlipType.RotateNoneFlipNone;
if (request.Hor)
{
rotation = RotateFlipType.RotateNoneFlipX;
}
object rotateObj = Interface.Call("GetImageRotation", request.Sign.Entity);
if (rotateObj is RotateFlipType)
{
rotation = (RotateFlipType)rotateObj;
}
// Get the bytes array for the resized image for the targeted sign.
byte[] resizedImageBytes = imageBytes.ResizeImage(size.Width, size.Height, size.ImageWidth, size.ImageHeight, signArtist.Settings.EnforceJpeg && !request.Raw, rotation);
// Verify that the resized file doesn't exceed the maximum configured filesize.
if (resizedImageBytes.Length > signArtist.Settings.MaxFileSizeInBytes)
{
// The file is too large, show a message to the player and attempt to start the next download.
signArtist.SendMessage(request.Sender, "FileTooLarge", signArtist.Settings.MaxSize);
www.Dispose();
StartNextDownload(true);
yield break;
}
// Ensure the texture id array is initialized to avoid errors.
request.Sign.EnsureInitialized();
// Check if the sign already has a texture assigned at the specified index.
uint existingTextureId = request.Sign.TextureIds[request.TextureIndex];
if (existingTextureId != 0)
{
// A texture was already assigned, remove this file to make room for the new one.
FileStorage.server.Remove(existingTextureId, FileStorage.Type.png, request.Sign.NetId);
}
uint textureId = FileStorage.server.Store(resizedImageBytes, FileStorage.Type.png, request.Sign.NetId, request.TextureIndex);
// Create the image on the filestorage and send out a network update for the sign.
request.Sign.SetImage(request.TextureIndex, textureId);
request.Sign.SendNetworkUpdate();
// Notify the player that the image was loaded.
signArtist.SendMessage(request.Sender, "ImageLoaded");
// Call the Oxide hook 'OnSignUpdated' to notify other plugins of the update event.
Interface.Oxide.CallHook("OnSignUpdated", request.Sign.Entity, request.Sender);
if (request.Sender != null)
{
// Check if logging to console is enabled.
if (signArtist.Settings.ConsoleLogging)
{
// Console logging is enabled, show a message in the server console.
signArtist.Puts(signArtist.GetTranslation("LogEntry"), request.Sender.displayName,
request.Sender.userID, textureId, request.Sign.ShortPrefabName, request.Url);
}
// Check if logging to file is enabled.
if (signArtist.Settings.FileLogging)
{
// File logging is enabled, add an entry to the logfile.
signArtist.LogToFile("log",
string.Format(signArtist.GetTranslation("LogEntry"), request.Sender.displayName,
request.Sender.userID, textureId, request.Sign.ShortPrefabName,
request.Url), signArtist);
}
if (signArtist.Settings.Discordlogging)
{
// Discord logging is enabled, add an entry to the logfile.
StartCoroutine(LogToDiscord(request));
}
}
// Attempt to start the next download.
StartNextDownload(true);
www.Dispose();
}
private byte[] GetImageBytes(byte[] bytes)
{
Texture2D texture = new Texture2D(2, 2);
texture.LoadImage(bytes);
byte[] image;
if (texture.format == TextureFormat.ARGB32 && !signArtist.Settings.EnforceJpeg)
{
image = texture.EncodeToPNG();
}
else
{
image = texture.EncodeToJPG(signArtist.Settings.Quality);
}
DestroyImmediate(texture);
return image;
}
private IEnumerator LogToDiscord(DownloadRequest request)
{
BasePlayer player = request.Sender;
IPaintableEntity sign = request.Sign;
var msg = DiscordMessage(ConVar.Server.hostname, player.displayName, player.UserIDString, sign.ShortPrefabName, request.Url, sign.Entity.transform.position.ToString());
string jsonmsg = JsonConvert.SerializeObject(msg);
UnityWebRequest wwwpost = new UnityWebRequest(signArtist.Settings.DiscordWebhook, "POST");
byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(jsonmsg.ToString());
wwwpost.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
wwwpost.SetRequestHeader("Content-Type", "application/json");
yield return wwwpost.SendWebRequest();
if (wwwpost.result != UnityWebRequest.Result.Success)
{
signArtist.PrintError(wwwpost.error);
signArtist.PrintError(jsonmsg);
yield break;
}
wwwpost.Dispose();
}
private Message DiscordMessage(string servername, string playername, string userid, string itemname, string imgurl, string location)
{
string steamprofile = "https://steamcommunity.com/profiles/" + userid;
var fields = new List<Message.Fields>()
{
new Message.Fields("Player: " + playername, $"[{userid}]({steamprofile})", true),
new Message.Fields("Entity", itemname, true),
new Message.Fields("Image Url", imgurl, false),
new Message.Fields("Teleport position", "teleportpos " + location.Replace(" ", string.Empty), false)
};
var footer = new Message.Footer($"Logged @{DateTime.UtcNow:dd/MM/yy HH:mm:ss}");
var image = new Message.Image(imgurl);
var embeds = new List<Message.Embeds>()
{
new Message.Embeds("Server - " + servername, "A sign has been updated" , fields, footer, image)
};
Message msg = new Message(signArtist.Settings.DiscordUsername, signArtist.Settings.AvatarUrl, embeds);
return msg;
}
/// <summary>
/// Restores the image and adds it to the sign again.
/// </summary>
/// <param name="request">The requested <see cref="RestoreRequest"></see> instance. </param>
/// <returns></returns>
private IEnumerator RestoreImage(RestoreRequest request)
{
// Verify that there is a valid reference to the plugin from this class.
if (signArtist == null)
{
throw new NullReferenceException("signArtist");
}
byte[] imageBytes;
uint textureId = request.Sign.TextureIds[request.TextureIndex];
// Check if the sign already has a texture assigned to it.
if (textureId == 0)
{
// No texture was previously assigned, show a message to the player.
signArtist.SendMessage(request.Sender, "RestoreErrorOccurred");
StartNextRestore(true);
yield break;
}
// Cache the byte array of the currently stored file.
imageBytes = FileStorage.server.Get(textureId, FileStorage.Type.png, request.Sign.NetId);
ImageSize size = GetImageSizeFor(request.Sign);
// Verify that we have image size data for the targeted sign.
if (size == null)
{
// No data was found, show a message to the player and print a detailed message to the server console and attempt to start the next download.
signArtist.SendMessage(request.Sender, "ErrorOccurred");
signArtist.PrintWarning($"Couldn't find the required image size for {request.Sign.PrefabName}, please report this in the plugin's thread.");
StartNextRestore(true);
yield break;
}
// Remove the texture from the FileStorage.
FileStorage.server.Remove(textureId, FileStorage.Type.png, request.Sign.NetId);
// Get the bytes array for the resized image for the targeted sign.
byte[] resizedImageBytes = imageBytes.ResizeImage(size.Width, size.Height, size.ImageWidth, size.ImageHeight, signArtist.Settings.EnforceJpeg && !request.Raw);
// Create the image on the filestorage and send out a network update for the sign.
request.Sign.SetImage(request.TextureIndex, FileStorage.server.Store(resizedImageBytes, FileStorage.Type.png, request.Sign.NetId));
request.Sign.SendNetworkUpdate();
// Notify the player that the image was loaded.
signArtist.SendMessage(request.Sender, "ImageRestored");
// Call the Oxide hook 'OnSignUpdated' to notify other plugins of the update event.
Interface.Oxide.CallHook("OnSignUpdated", request.Sign.Entity, request.Sender);
// Attempt to start the next download.
StartNextRestore(true);
}
/// <summary>
/// Gets the target image size for a <see cref="Signage"></see>.
/// </summary>
/// <param name="signage"></param>
private ImageSize GetImageSizeFor(IPaintableEntity signage)
{
if (signArtist.ImageSizePerAsset.ContainsKey(signage.ShortPrefabName))
{
return signArtist.ImageSizePerAsset[signage.ShortPrefabName];
}
return null;
}
/// <summary>
/// Converts the <see cref="Texture2D"></see> from the webrequest to a <see cref="byte"></see> array.
/// </summary>
/// <param name="www">The completed webrequest. </param>
private byte[] GetImageBytes(UnityWebRequest www)
{
Texture2D texture = new Texture2D(2, 2);
texture.LoadImage(www.downloadHandler.data);
byte[] image;
if (texture.format == TextureFormat.ARGB32 && !signArtist.Settings.EnforceJpeg)
{
image = texture.EncodeToPNG();
}
else
{
image = texture.EncodeToJPG(signArtist.Settings.Quality);
}
DestroyImmediate(texture);
return image;
}
}
#endregion Image Download Behaviour
private interface IPaintableEntity
{
BaseEntity Entity { get; }
string PrefabName { get; }
string ShortPrefabName { get; }
NetworkableId NetId { get; }
int TextureCount { get; }
uint[] TextureIds { get; }
void SendNetworkUpdate();
void SetImage(uint textureIndex, uint id);
bool CanUpdate(BasePlayer player);
void EnsureInitialized();
}
private abstract class BasePaintableEntity : IPaintableEntity
{
public BaseEntity Entity { get; }
public string PrefabName { get; }
public string ShortPrefabName { get; }
public NetworkableId NetId { get; }
public virtual int TextureCount => 1;
public abstract uint[] TextureIds { get; }
protected BasePaintableEntity(BaseEntity entity)
{
Entity = entity;
PrefabName = Entity.PrefabName;
ShortPrefabName = Entity.ShortPrefabName;
NetId = Entity.net.ID;
}
public abstract bool CanUpdate(BasePlayer player);
public abstract void SetImage(uint textureIndex, uint id);
public virtual void EnsureInitialized() { }
public void SendNetworkUpdate()
{
Entity.SendNetworkUpdate();
}
}
private class PaintableSignage : BasePaintableEntity, IPaintableEntity
{
public Signage Sign { get; set; }
public override int TextureCount => Sign.TextureCount;
public override uint[] TextureIds => Sign.GetTextureCRCs();
public PaintableSignage(Signage sign) : base(sign)
{
Sign = sign;
}
public override void SetImage(uint textureIndex, uint id)
{
Sign.EnsureInitialized();
Sign.textureIDs[textureIndex] = id;
}
public override bool CanUpdate(BasePlayer player)
{
return Sign.CanUpdateSign(player);
}
public override void EnsureInitialized()
{
Sign.EnsureInitialized();
}
}
private class PaintableFrame : BasePaintableEntity, IPaintableEntity
{
public PhotoFrame Sign { get; set; }
public override uint[] TextureIds => Sign.GetTextureCRCs();
public PaintableFrame(PhotoFrame sign) : base(sign)
{
Sign = sign;
}
public override void SetImage(uint textureIndex, uint id)
{
Sign._overlayTextureCrc = id;
}
public override bool CanUpdate(BasePlayer player)
{
return Sign.CanUpdateSign(player);
}
}
private class PaintablePumpkin : BasePaintableEntity, IPaintableEntity
{
public CarvablePumpkin Pumpkin { get; set; }
public override uint[] TextureIds => Pumpkin.textureIDs;
public PaintablePumpkin(CarvablePumpkin pumpkin) : base(pumpkin)
{
Pumpkin = pumpkin;
}
public override void SetImage(uint textureIndex, uint id)
{
EnsureInitialized();
Pumpkin.textureIDs[textureIndex] = id;
}
public override bool CanUpdate(BasePlayer player)
{
return Pumpkin.CanUpdateSign(player);
}
public override void EnsureInitialized()
{
int size = Mathf.Max(Pumpkin.paintableSources.Length, 1);
if (Pumpkin.textureIDs == null || Pumpkin.textureIDs.Length != size)
{
Array.Resize(ref Pumpkin.textureIDs, size);
}
}
}
#region Init
/// <summary>
/// Oxide hook that is triggered when the plugin is loaded.
/// </summary>
///
private void Init()
{
// Register all the permissions used by the plugin
permission.RegisterPermission("signartist.file", this);
permission.RegisterPermission("signartist.ignorecd", this);
permission.RegisterPermission("signartist.ignoreowner", this);
permission.RegisterPermission("signartist.raw", this);
permission.RegisterPermission("signartist.restore", this);
permission.RegisterPermission("signartist.restoreall", this);
permission.RegisterPermission("signartist.text", this);
permission.RegisterPermission("signartist.url", this);
AddCovalenceCommand("sil", "SilCommand");
AddCovalenceCommand("silt", "SiltCommand");
AddCovalenceCommand("sili", "SilItemCommand");
AddCovalenceCommand("silrestore", "RestoreCommand");
// Initialize the dictionary with all paintable object assets and their target sizes
// Initialize the dictionary with all paintable object assets and their target sizes
ImageSizePerAsset = new Dictionary<string, ImageSize>
{
// Picture Frames
["sign.pictureframe.landscape"] = new ImageSize(256, 192), // Landscape Picture Frame
["sign.pictureframe.portrait"] = new ImageSize(205, 256), // Portrait Picture Frame
["sign.pictureframe.tall"] = new ImageSize(128, 512), // Tall Picture Frame
["sign.pictureframe.xl"] = new ImageSize(512, 512), // XL Picture Frame
["sign.pictureframe.xxl"] = new ImageSize(1024, 512), // XXL Picture Frame
// Wooden Signs
["sign.small.wood"] = new ImageSize(256, 128), // Small Wooden Sign
["sign.medium.wood"] = new ImageSize(512, 256), // Wooden Sign
["sign.large.wood"] = new ImageSize(512, 256), // Large Wooden Sign
["sign.huge.wood"] = new ImageSize(1024, 256), // Huge Wooden Sign
// Banners
["sign.hanging.banner.large"] = new ImageSize(256, 1024), // Large Banner Hanging
["sign.pole.banner.large"] = new ImageSize(256, 1024), // Large Banner on Pole
// Hanging Signs
["sign.hanging"] = new ImageSize(256, 512), // Two Sided Hanging Sign
["sign.hanging.ornate"] = new ImageSize(512, 256), // Two Sided Ornate Hanging Sign
// Town Signs
["sign.post.single"] = new ImageSize(256, 128), // Single Sign Post
["sign.post.double"] = new ImageSize(512, 512), // Double Sign Post
["sign.post.town"] = new ImageSize(512, 256), // One Sided Town Sign Post
["sign.post.town.roof"] = new ImageSize(512, 256), // Two Sided Town Sign Post
// Photo Frames
["photoframe.large"] = new ImageSize(320, 240),
["photoframe.portrait"] = new ImageSize(320, 384),
["photoframe.landscape"] = new ImageSize(320, 240),
// Neon Signs
["sign.neon.xl.animated"] = new ImageSize(256, 256),
["sign.neon.xl"] = new ImageSize(256, 256),
["sign.neon.125x215.animated"] = new ImageSize(128, 256),
["sign.neon.125x215"] = new ImageSize(128, 256),
["sign.neon.125x125"] = new ImageSize(128, 128),
// Other paintable assets
["spinner.wheel.deployed"] = new ImageSize(512, 512, 285, 285), // Spinning Wheel
["carvable.pumpkin"] = new ImageSize(256, 256),
//Artist DLC
["lightupframe.small"] = new ImageSize(128, 175),
["lightupframe.xl"] = new ImageSize(512, 512),
["lightupframe.large"] = new ImageSize(320, 256),
["lightupframe.standing"] = new ImageSize(128, 175),
["goldframe.small"] = new ImageSize(256, 1024),
["goldframe.standing"] = new ImageSize(128, 320),
["goldframe.xxl"] = new ImageSize(1024, 512),
["lightupframe.xxl"] = new ImageSize(1024, 512),
["scrapframe.small"] = new ImageSize(128, 175),
["paintable_reactive_target.deployed"] = new ImageSize(256, 256),
["window.paintable"] = new ImageSize(512, 256),
["goldframe.large"] = new ImageSize(320, 256),
["scrapframe.large"] = new ImageSize(256, 1024),
["goldframe.xl"] = new ImageSize(512, 512),
["scrapframe.standing"] = new ImageSize(128, 320),
["scrapframe.xl"] = new ImageSize(512, 512),
["scrapframe.xxl"] = new ImageSize(1024, 512),
["sign.artistcanvas.xxl"] = new ImageSize(1024, 512),
["sign.artistcanvas.xl"] = new ImageSize(512, 512),
["sign.artistcanvas.l"] = new ImageSize(256, 640),
["sign.artistcanvas.xs"] = new ImageSize(192, 256),
["sign.artistcanvas.m"] = new ImageSize(320, 240),
};
}
private void GetSteamworksImages()
{
foreach (InventoryDef item in Steamworks.SteamInventory.Definitions)
{
string shortname = item.GetProperty("itemshortname");
if (item == null || string.IsNullOrEmpty(shortname))
continue;
if (item.Id < 100)
continue;
ulong workshopid;
if (!ulong.TryParse(item.GetProperty("workshopid"), out workshopid))
continue;
if (string.IsNullOrEmpty(item.IconUrl)) continue;
SkiniconUrls[workshopid] = item.IconUrl;
}
}
/// <summary>
/// Oxide hook that is triggered to automatically load the configuration file.
/// </summary>
protected override void LoadConfig()
{
base.LoadConfig();
Settings = Config.ReadObject<SignArtistConfig>();
SaveConfig();
}
/// <summary>
/// Oxide hook that is triggered to automatically load the default configuration file when no file exists.
/// </summary>
protected override void LoadDefaultConfig()
{
Settings = SignArtistConfig.DefaultConfig();
}
/// <summary>
/// Oxide hook that is triggered to save the configuration file.
/// </summary>
protected override void SaveConfig()
{
Config.WriteObject(Settings);
}
/// <summary>
/// Oxide hook that is triggered when the server has fully initialized.
/// </summary>
private void OnServerInitialized()
{
// Create a new GameObject and attach the UnityEngine script to it for handling the image downloads.
imageDownloaderGameObject = new GameObject("ImageDownloader");
imageDownloader = imageDownloaderGameObject.AddComponent<ImageDownloader>();
if ((Steamworks.SteamInventory.Definitions?.Length ?? 0) == 0)
{
PrintWarning("Waiting for Steamworks to update item definitions....");
Steamworks.SteamInventory.OnDefinitionsUpdated += GetSteamworksImages;
}
else GetSteamworksImages();
}
/// <summary>
/// Oxide hook that is triggered when the plugin is unloaded.
/// </summary>
private void Unload()
{
// Destroy the created GameObject and cleanup.
UnityEngine.Object.Destroy(imageDownloaderGameObject);
imageDownloader = null;
cooldowns = null;
Steamworks.SteamInventory.OnDefinitionsUpdated -= GetSteamworksImages;
}
/// <summary>
/// Handles the /sil command.
/// </summary>
/// <param name="iplayer">The player that has executed the command. </param>
/// <param name="command">The name of the command that was executed. </param>
/// <param name="args">All arguments that were passed with the command. </param>
///
#endregion Init
#region Localization
/// <summary>
/// Oxide hook that is triggered automatically after it has been loaded to initialize the messages for the Lang API.
/// </summary>
protected override void LoadDefaultMessages()
{
// Register all messages used by the plugin in the Lang API.
lang.RegisterMessages(new Dictionary<string, string>
{
// Messages used throughout the plugin.
["WebErrorOccurred"] = "Failed to download the image! Error {0}.",
["FileTooLarge"] = "The file exceeds the maximum file size of {0}Mb.",
["ErrorOccurred"] = "An unknown error has occured, if this error keeps occuring please notify the server admin.",
["RestoreErrorOccurred"] = "Can't restore the sign because no texture is assigned to it.",
["DownloadQueued"] = "Your image was added to the download queue!",
["RestoreQueued"] = "Your sign was added to the restore queue!",
["RestoreBatchQueued"] = "You added all {0} signs to the restore queue!",
["ImageLoaded"] = "The image was succesfully loaded to the sign!",
["ImageRestored"] = "The image was succesfully restored for the sign!",
["LogEntry"] = "Player `{0}` (SteamId: {1}) loaded {2} into {3} from {4}",
["NoSignFound"] = "Unable to find a sign! Make sure you are looking at one and that you are not too far away from it.",
["Cooldown"] = "You can't use the command yet! Remaining cooldown: {0}.",
["SignNotOwned"] = "You can't change this sign as it is protected by a tool cupboard.",
["NoItemHeld"] = "You're not holding an item.",
["ActionQueuedAlready"] = "An action has already been queued for this sign, please wait for this action to complete.",
["SyntaxSilCommand"] = "Syntax error!\nSyntax: /sil <url> [raw]",
["SyntaxSilCommandMulti"] = "Syntax error!\nSyntax: /sil <1-{0}> <url> [raw]",
["SyntaxSiltCommand"] = "Syntax error!\nSyntax: /silt <message> [<fontsize:number>] [<color:hex value>] [<bgcolor:hex value>] [raw]",
["SyntaxSiltCommandMulti"] = "Syntax error!\nSyntax: /silt <1-{0}> <message> [<fontsize:number>] [<color:hex value>] [<bgcolor:hex value>] [raw]",
["SyntaxSiliCommandMulti"] = "Syntax error!\nSyntax: /sili <1-{0}>",
["NoPermission"] = "You don't have permission to use this command.",
["NoPermissionFile"] = "You don't have permission to use images from the server's filesystem.",
["NoPermissionRaw"] = "You don't have permission to use raw images, loading normally instead.",
["NoPermissionRestoreAll"] = "You don't have permission to use restore all signs at once.",
// Cooldown formatting 'translations'.
["day"] = "day",
["days"] = "days",
["hour"] = "hour",
["hours"] = "hours",
["minute"] = "minute",
["minutes"] = "minutes",
["second"] = "second",
["seconds"] = "seconds",
["and"] = "and"
}, this);
}
#endregion Localization
#region Commands
[Command("sil"), Permission("signartist.url")]
private void SilCommand(IPlayer iplayer, string command, string[] args)
{
var player = iplayer.Object as BasePlayer;
// Verify if the player has permission to use this command.
if (!HasPermission(player, "signartist.url"))
{
// The player doesn't have permission to use this command
// ... [Code preview truncated due to length. Full source in attached file] ...