After the server restarts, pictures put up from /sil disappear leaving the wood a blank canvas.. what's happening
Wood signs have the pictures disappear
same ... after game-, and / or serverrestart all pics are lost !
Small solution i found, they've stayed after i "locked edits" on the signNickPhaxveyon
same ... after game-, and / or serverrestart all pics are lost !
... if i locked, the pics lost too after some restarts from server or game ! sorry, this is not the solution :)
Merged post
// 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 + persistence patch", "1.4.7")]
[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>();
// Persistent source information for sign images. Rust FileStorage stores the actual image,
// but the texture id can become invalid after a restart. Keeping the source URL lets us
// rebuild the FileStorage entry automatically.
private const string PersistenceFileName = "SignArtistPersistence";
private SignArtistPersistenceData Persistence { get; set; }
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"
};
}
}
private class SignArtistPersistenceData
{
public List<PersistedImage> Images { get; set; } = new List<PersistedImage>();
}
private class PersistedImage
{
public ulong NetId { get; set; }
public uint TextureIndex { get; set; }
public string Url { get; set; }
public bool Raw { get; set; }
}
/// <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);
http://www.timeout = 10;
yield return http://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", http://www.error);
http://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);
http://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);
http://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);
http://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);
if (textureId == 0)
{
signArtist.PrintError($"FileStorage.Store returned 0 for {request.Sign.ShortPrefabName} ({request.Sign.NetId}).");
StartNextDownload(true);
http://www.Dispose();
yield break;
}
// Persist the source before notifying clients. If Rust later loses the FileStorage
// entry, the plugin can download the image again automatically after restart.
signArtist.SavePersistedImage(request.Sign.NetId, request.TextureIndex, request.Url, request.Raw);
// 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);
http://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.
uint newTextureId = FileStorage.server.Store(resizedImageBytes, FileStorage.Type.png, request.Sign.NetId, request.TextureIndex);
if (newTextureId == 0)
{
signArtist.PrintError($"FileStorage.Store returned 0 while restoring {request.Sign.ShortPrefabName} ({request.Sign.NetId}).");
StartNextRestore(true);
yield break;
}
request.Sign.SetImage(request.TextureIndex, newTextureId);
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()
{
// Immediate update is important here: the texture id has just changed and
// should be pushed to Rust before a save/restart can race the normal update queue.
Entity.SendNetworkUpdateImmediate();
}
}
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()
{
LoadPersistence();
// 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();
// Wait until the saved entities are fully available, then verify their FileStorage
// references and re-download anything that was lost.
timer.Once(5f, RestorePersistedImages);
}
private void LoadPersistence()
{
try
{
Persistence = Interface.Oxide.DataFileSystem.ReadObject<SignArtistPersistenceData>(PersistenceFileName);
if (Persistence == null)
Persistence = new SignArtistPersistenceData();
if (Persistence.Images == null)
Persistence.Images = new List<PersistedImage>();
}
catch (Exception ex)
{
PrintError($"Failed to load {PersistenceFileName}.json: {ex.Message}");
Persistence = new SignArtistPersistenceData();
}
}
private void SavePersistence()
{
try
{
Interface.Oxide.DataFileSystem.WriteObject(PersistenceFileName, Persistence);
}
catch (Exception ex)
{
PrintError($"Failed to save {PersistenceFileName}.json: {ex.Message}");
}
}
private void SavePersistedImage(NetworkableId netId, uint textureIndex, string url, bool raw)
{
if (Persistence == null)
Persistence = new SignArtistPersistenceData();
ulong id = netId.Value;
PersistedImage entry = Persistence.Images.FirstOrDefault(x => x.NetId == id && x.TextureIndex == textureIndex);
if (entry == null)
{
entry = new PersistedImage
{
NetId = id,
TextureIndex = textureIndex
};
Persistence.Images.Add(entry);
}
entry.Url = url;
entry.Raw = raw;
SavePersistence();
}
private void RemovePersistedImage(ulong netId, uint textureIndex)
{
if (Persistence?.Images == null)
return;
int removed = Persistence.Images.RemoveAll(x => x.NetId == netId && x.TextureIndex == textureIndex);
if (removed > 0)
SavePersistence();
}
private BaseEntity FindEntity(ulong netId)
{
NetworkableId id = new NetworkableId(netId);
Signage[] signs = UnityEngine.Object.FindObjectsOfType<Signage>();
foreach (Signage sign in signs)
{
if (sign != null && sign.net.ID == id)
return sign;
}
PhotoFrame[] frames = UnityEngine.Object.FindObjectsOfType<PhotoFrame>();
foreach (PhotoFrame frame in frames)
{
if (frame != null && frame.net.ID == id)
return frame;
}
CarvablePumpkin[] pumpkins = UnityEngine.Object.FindObjectsOfType<CarvablePumpkin>();
foreach (CarvablePumpkin pumpkin in pumpkins)
{
if (pumpkin != null && pumpkin.net.ID == id)
return pumpkin;
}
return null;
}
private void RestorePersistedImages()
{
if (Persistence?.Images == null || Persistence.Images.Count == 0 || imageDownloader == null)
return;
int queued = 0;
List<PersistedImage> stale = new List<PersistedImage>();
foreach (PersistedImage entry in Persistence.Images.ToList())
{
if (string.IsNullOrEmpty(entry.Url))
{
stale.Add(entry);
continue;
}
BaseEntity entity = FindEntity(entry.NetId);
if (entity == null)
continue;
IPaintableEntity paintable = null;
if (entity is Signage) paintable = new PaintableSignage((Signage)entity);
else if (entity is PhotoFrame) paintable = new PaintableFrame((PhotoFrame)entity);
else if (entity is CarvablePumpkin) paintable = new PaintablePumpkin((CarvablePumpkin)entity);
if (paintable == null || entry.TextureIndex >= paintable.TextureCount)
continue;
paintable.EnsureInitialized();
uint textureId = paintable.TextureIds[entry.TextureIndex];
bool missing = textureId == 0;
if (!missing)
{
try
{
byte[] stored = FileStorage.server.Get(textureId, FileStorage.Type.png, paintable.NetId);
missing = stored == null || stored.Length == 0;
}
catch
{
missing = true;
}
}
if (!missing)
continue;
imageDownloader.QueueDownload(entry.Url, null, paintable, entry.TextureIndex, entry.Raw, paintable.ShortPrefabName == "sign.hanging");
queued++;
}
foreach (PersistedImage entry in stale)
Persistence.Images.Remove(entry);
if (stale.Count > 0)
SavePersistence();
if (queued > 0)
Puts($"Queued {queued} persisted sign image(s) for automatic restoration.");
}
/// <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, show an error message.
SendMessage(player, "NoPermission");
return;
}
// Verify that the command isn't on cooldown for the user.
if (HasCooldown(player))
{
// The command is still on cooldown for the player, show an error message.
SendMessage(player, "Cooldown", FormatCooldown(GetCooldown(player)));
return;
}
// Check if the player is looking at a sign.
IPaintableEntity sign;
if (!IsLookingAtSign(player, out sign))
{
// The player isn't looking at a sign or is too far away from it, show an error message.
SendMessage(player, "NoSignFound");
return;
}
uint textureIndex = 0;
if (sign.TextureCount > 1)
{
// Verify if the correct syntax is used for animated Neon Signs.
if (!HasValidIndexArg(args, sign, out textureIndex))
{
// Invalid syntax was used, show an error message to the player.
SendMessage(player, "SyntaxSilCommandMulti", sign.TextureCount);
return;
}
// Remove the index argument so the rest of the command handling can ignore it.
args = args.Skip(1).ToArray();
}
// Verify if the correct syntax is used.
if (args.Length < 1)
{
// Invalid syntax was used, show an error message to the player.
if (sign.TextureCount > 1)
{
SendMessage(player, "SyntaxSilCommandMulti", sign.TextureCount);
}
else
{
SendMessage(player, "SyntaxSilCommand");
}
return;
}
// Check if the player is able to update the sign.
if (!CanChangeSign(player, sign))
{
// The player isn't able to update the sign, show an error message.
SendMessage(player, "SignNotOwned");
return;
}
// Check if the player wants to add the image from the server's filesystem and has the permission to do so.
if (args[0].StartsWith("file://") && !HasPermission(player, "signartist.file"))
{
// The player doesn't have permission for this, show an error message.
SendMessage(player, "NoPermissionFile");
return;
}
// Check if the player wants to add the image as a raw image and has the permission to do so.
bool raw = args.Length > 1 && args[1].Equals("raw", StringComparison.OrdinalIgnoreCase);
if (raw && !HasPermission(player, "signartist.raw"))
{
// The player doesn't have permission for this, show a message and disable raw.
SendMessage(player, "NoPermissionRaw");
raw = false;
}
// This sign pastes in reverse, so we'll check and set a var to flip it
bool hor = sign.ShortPrefabName == "sign.hanging";
// Notify the player that it is added to the queue.
SendMessage(player, "DownloadQueued");
// Queue the download of the specified image.
imageDownloader.QueueDownload(args[0], player, sign, textureIndex, raw, hor);
// Call external hook
Interface.Oxide.CallHook("OnImagePost", player, args[0], raw, sign.Entity, textureIndex);
// Set the cooldown on the command for the player if the cooldown setting is enabled.
SetCooldown(player);
}
[Command("sili"), Permission("signartist.url")]
private void SilItemCommand(IPlayer iplayer, string command, string[] args)
{
var player = iplayer.Object as BasePlayer;
if (!HasPermission(player, "signartist.url"))
{
SendMessage(player, "NoPermission");
return;
}
if (HasCooldown(player))
{
SendMessage(player, "Cooldown", FormatCooldown(GetCooldown(player)));
return;
}
IPaintableEntity sign;
if (!IsLookingAtSign(player, out sign))
{
SendMessage(player, "NoSignFound");
return;
}
if (!CanChangeSign(player, sign))
{
SendMessage(player, "SignNotOwned");
return;
}
Item held = player.GetActiveItem();
if (held == null)
{
SendMessage(player, "NoItemHeld");
return;
}
uint textureIndex = 0;
if (sign.TextureCount > 1)
{
// Verify if the correct syntax is used for animated Neon Signs.
if (!HasValidIndexArg(args, sign, out textureIndex))
{
// Invalid syntax was used, show an error message to the player.
SendMessage(player, "SyntaxSiliCommandMulti", sign.TextureCount);
return;
}
// Remove the index argument so the rest of the command handling can ignore it.
args = args.Skip(1).ToArray();
}
string shortname = held.info.shortname;
bool hor = sign.ShortPrefabName == "sign.hanging";
SendMessage(player, "DownloadQueued");
bool defaultskin = false;
if (args.Length == 1 && args[0] == "default") defaultskin = true;
if (held.skin != 0uL && !defaultskin)
{
string url;
if (SkiniconUrls.TryGetValue(held.skin, out url))
{
shortname = url;
}
else
{
ServerMgr.Instance.StartCoroutine(DownloadWorkshopskin(held, sign, hor, textureIndex));
return;
} xlr8tednergySmall solution i found, they've stayed after i "locked edits" on the sign
... dont use the code, i will send an updated .cs !
Merged post
... use this modded .cs:
https://www.file-upload.net/download-15609798/SignArtist.cs.html
it create an File in /DATA of Server, where saved all used Picture and after ServerRestart it send it back to ure Game.
Please tell me, it is working or not (on my Server it works fine) 😊