· 5 years ago · Feb 15, 2021, 03:56 AM
1// Reference: System.Drawing
2using Newtonsoft.Json;
3using Oxide.Core;
4using Oxide.Plugins.SignArtistClasses;
5using System;
6using System.Collections;
7using System.Collections.Generic;
8using System.Drawing;
9using System.Drawing.Drawing2D;
10using System.Drawing.Imaging;
11using System.IO;
12using System.Linq;
13using Oxide.Core.Libraries.Covalence;
14using UnityEngine;
15using UnityEngine.Networking;
16using Color = System.Drawing.Color;
17using Graphics = System.Drawing.Graphics;
18using Steamworks;
19
20namespace Oxide.Plugins
21{
22 [Info("Sign Artist Mod", "bmgjet", "1.2.6")]
23 [Description("Allows players with the appropriate permission to import images from the internet on paintable objects")]
24
25 /*********************************************************************************
26 * This plugin was originally created by Bombardir and then maintained by Nogrod.
27 * It was rewritten from scratch by Mughisi on January 12th, 2018.
28 *********************************************************************************/
29
30 internal class SignArtist : RustPlugin
31 {
32 private Dictionary<ulong, float> cooldowns = new Dictionary<ulong, float>();
33 private GameObject imageDownloaderGameObject;
34 private ImageDownloader imageDownloader;
35 SignArtistConfig Settings { get; set; }
36 Dictionary<string, ImageSize> ImageSizePerAsset { get; set; }
37
38 Dictionary<ulong, string> SkiniconUrls = new Dictionary<ulong, string>();
39
40 private const string ItemIconUrl = "https://www.rustedit.io/images/imagelibrary/{0}.png";
41
42
43 /// <summary>
44 /// Plugin configuration
45 /// </summary>
46 public class SignArtistConfig
47 {
48 [JsonProperty(PropertyName = "Time in seconds between download requests (0 to disable)")]
49 public int Cooldown { get; set; }
50
51 [JsonProperty(PropertyName = "Maximum concurrent downloads")]
52 public int MaxActiveDownloads { get; set; }
53
54 [JsonProperty(PropertyName = "Maximum distance from the sign")]
55 public int MaxDistance { get; set; }
56
57 [JsonProperty(PropertyName = "Maximum filesize in MB")]
58 public float MaxSize { get; set; }
59
60 [JsonProperty(PropertyName = "Enforce JPG file format")]
61 public bool EnforceJpeg { get; set; }
62
63 [JsonProperty(PropertyName = "JPG image quality (0-100)")]
64 public int Quality
65 {
66 get
67 {
68 return quality;
69 }
70 set
71 {
72 // Validate the value, it can't be less than 0 and not more than 100.
73 if (value >= 0 && value <= 100)
74 {
75 quality = value;
76 }
77 else
78 {
79 // Set the quality to a default value of 85% when an invalid value was specified.
80 quality = value > 100 ? 100 : 85;
81 }
82 }
83 }
84
85 [JsonProperty("Enable logging file")]
86 public bool FileLogging { get; set; }
87
88 [JsonProperty("Enable logging console")]
89 public bool ConsoleLogging { get; set; }
90
91 [JsonProperty("Enable discord logging")]
92 public bool Discordlogging { get; set; }
93
94 [JsonProperty("Discord Webhook")]
95 public string DiscordWebhook { get; set; }
96
97 [JsonProperty("Avatar URL")]
98 public string AvatarUrl { get; set; }
99
100 [JsonProperty("Discord Username")]
101 public string DiscordUsername { get; set; }
102
103
104 [JsonIgnore]
105 public float MaxFileSizeInBytes
106 {
107 get
108 {
109 return MaxSize * 1024 * 1024;
110 }
111 }
112
113 private int quality = 85;
114
115 /// <summary>
116 /// Creates a default configuration file
117 /// </summary>
118 /// <returns>Default config</returns>
119 public static SignArtistConfig DefaultConfig()
120 {
121 return new SignArtistConfig
122 {
123 Cooldown = 0,
124 MaxSize = 3,
125 MaxDistance = 5,
126 MaxActiveDownloads = 5,
127 EnforceJpeg = false,
128 Quality = 95,
129 FileLogging = false,
130 ConsoleLogging = false,
131 Discordlogging = false,
132 DiscordWebhook = "",
133 AvatarUrl = "https://i.imgur.com/dH7V1Dh.png",
134 DiscordUsername = "Sign Artist"
135 };
136 }
137 }
138
139 /// <summary>
140 /// A type used to request new images to download.
141 /// </summary>
142 private class DownloadRequest
143 {
144 public BasePlayer Sender { get; }
145 public IPaintableEntity Sign { get; }
146 public string Url { get; set; }
147 public bool Raw { get; }
148 public bool Hor { get; }
149
150 /// <summary>
151 /// Initializes a new instance of the <see cref="DownloadRequest" /> class.
152 /// </summary>
153 /// <param name="url">The URL to download the image from. </param>
154 /// <param name="player">The player that requested the download. </param>
155 /// <param name="sign">The sign to add the image to. </param>
156 /// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
157 public DownloadRequest(string url, BasePlayer player, IPaintableEntity sign, bool raw, bool hor)
158 {
159 Url = url;
160 Sender = player;
161 Sign = sign;
162 Raw = raw;
163 Hor = hor;
164 }
165 }
166
167 /// <summary>
168 /// A type used to request new images to be restored.
169 /// </summary>
170 private class RestoreRequest
171 {
172 public BasePlayer Sender { get; }
173 public IPaintableEntity Sign { get; }
174 public bool Raw { get; }
175
176 /// <summary>
177 /// Initializes a new instance of the <see cref="RestoreRequest" /> class.
178 /// </summary>
179 /// <param name="player">The player that requested the restore. </param>
180 /// <param name="sign">The sign to restore the image from. </param>
181 /// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
182 public RestoreRequest(BasePlayer player, IPaintableEntity sign, bool raw)
183 {
184 Sender = player;
185 Sign = sign;
186 Raw = raw;
187 }
188 }
189
190 /// <summary>
191 /// A type used to determine the size of the image for a sign
192 /// </summary>
193 public class ImageSize
194 {
195 public int Width { get; }
196 public int Height { get; }
197 public int ImageWidth { get; }
198 public int ImageHeight { get; }
199
200 /// <summary>
201 /// Initializes a new instance of the <see cref="ImageSize" /> class.
202 /// </summary>
203 /// <param name="width">The width of the canvas and the image. </param>
204 /// <param name="height">The height of the canvas and the image. </param>
205 public ImageSize(int width, int height) : this(width, height, width, height)
206 {
207 }
208
209 /// <summary>
210 /// Initializes a new instance of the <see cref="ImageSize" /> class.
211 /// </summary>
212 /// <param name="width">The width of the canvas. </param>
213 /// <param name="height">The height of the canvas. </param>
214 /// <param name="imageWidth">The width of the image. </param>
215 /// <param name="imageHeight">The height of the image. </param>
216 public ImageSize(int width, int height, int imageWidth, int imageHeight)
217 {
218 Width = width;
219 Height = height;
220 ImageWidth = imageWidth;
221 ImageHeight = imageHeight;
222 }
223 }
224
225
226 #region Image Download Behaviour
227 /// <summary>
228 /// UnityEngine script to be attached to a GameObject to download images and apply them to signs.
229 /// </summary>
230 private class ImageDownloader : MonoBehaviour
231 {
232 private byte activeDownloads;
233 private byte activeRestores;
234 private readonly SignArtist signArtist = (SignArtist)Interface.Oxide.RootPluginManager.GetPlugin(nameof(SignArtist));
235 private readonly Queue<DownloadRequest> downloadQueue = new Queue<DownloadRequest>();
236 private readonly Queue<RestoreRequest> restoreQueue = new Queue<RestoreRequest>();
237
238 /// <summary>
239 /// Queue a new image to download and add to a sign
240 /// </summary>
241 /// <param name="url">The URL to download the image from. </param>
242 /// <param name="player">The player that requested the download. </param>
243 /// <param name="sign">The sign to add the image to. </param>
244 /// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
245 public void QueueDownload(string url, BasePlayer player, IPaintableEntity sign, bool raw, bool hor = false)
246 {
247 // Check if there is already a request for this sign and show an error if there is.
248 bool existingRequest = downloadQueue.Any(request => request.Sign == sign) || restoreQueue.Any(request => request.Sign == sign);
249 if (existingRequest)
250 {
251 signArtist.SendMessage(player, "ActionQueuedAlready");
252
253 return;
254 }
255
256 // Instantiate a new DownloadRequest and add it to the queue.
257 downloadQueue.Enqueue(new DownloadRequest(url, player, sign, raw, hor));
258
259 // Attempt to start the next download.
260 StartNextDownload();
261 }
262
263 /// <summary>
264 /// Attempts to restore a sign.
265 /// </summary>
266 /// <param name="player">The player that requested the restore. </param>
267 /// <param name="sign">The sign to restore the image from. </param>
268 /// <param name="raw">Should the image be stored with or without conversion to jpeg. </param>
269 public void QueueRestore(BasePlayer player, IPaintableEntity sign, bool raw)
270 {
271 // Check if there is already a request for this sign and show an error if there is.
272 bool existingRequest = downloadQueue.Any(request => request.Sign == sign) || restoreQueue.Any(request => request.Sign == sign);
273 if (existingRequest)
274 {
275 signArtist.SendMessage(player, "ActionQueuedAlready");
276
277 return;
278 }
279
280 // Instantiate a new RestoreRequest and add it to the queue.
281 restoreQueue.Enqueue(new RestoreRequest(player, sign, raw));
282
283 // Attempt to start the next restore.
284 StartNextRestore();
285 }
286
287 /// <summary>
288 /// Starts the next download if available.
289 /// </summary>
290 /// <param name="reduceCount"></param>
291 private void StartNextDownload(bool reduceCount = false)
292 {
293 // Check if we need to reduce the active downloads counter after a succesful or failed download.
294 if (reduceCount)
295 {
296 activeDownloads--;
297 }
298
299 // Check if we don't have the maximum configured amount of downloads running already.
300 if (activeDownloads >= signArtist.Settings.MaxActiveDownloads)
301 {
302 return;
303 }
304
305 // Check if there is still an image in the queue.
306 if (downloadQueue.Count <= 0)
307 {
308 return;
309 }
310
311 // Increment the active downloads by 1 and start the download process.
312 activeDownloads++;
313 StartCoroutine(DownloadImage(downloadQueue.Dequeue()));
314 }
315
316 /// <summary>
317 /// Starts the next restore if available.
318 /// </summary>
319 /// <param name="reduceCount"></param>
320 private void StartNextRestore(bool reduceCount = false)
321 {
322 // Check if we need to reduce the active restores counter after a succesful or failed restore.
323 if (reduceCount)
324 {
325 activeRestores--;
326 }
327
328 // Check if we don't have the maximum configured amount of restores running already.
329 if (activeRestores >= signArtist.Settings.MaxActiveDownloads)
330 {
331 return;
332 }
333
334 // Check if there is still an image in the queue.
335 if (restoreQueue.Count <= 0)
336 {
337 return;
338 }
339
340 // Increment the active restores by 1 and start the restore process.
341 activeRestores++;
342 StartCoroutine(RestoreImage(restoreQueue.Dequeue()));
343 }
344
345
346
347 /// <summary>
348 /// Downloads the image and adds it to the sign.
349 /// </summary>
350 /// <param name="request">The requested <see cref="DownloadRequest"/> instance. </param>
351 private IEnumerator DownloadImage(DownloadRequest request)
352 {
353 int fselected = 0;
354 if (request.Url.StartsWith("frame:0"))
355 {
356 fselected = 0;
357 request.Url = request.Url.Replace("frame:0","");
358 }
359 else if (request.Url.StartsWith("frame:1"))
360 {
361 fselected = 1;
362 request.Url = request.Url.Replace("frame:1","");
363 }
364 else if (request.Url.StartsWith("frame:2"))
365 {
366 fselected = 2;
367 request.Url = request.Url.Replace("frame:2","");
368 }
369 else if (request.Url.StartsWith("frame:3"))
370 {
371 fselected = 3;
372 request.Url = request.Url.Replace("frame:3","");
373 }
374 else if (request.Url.StartsWith("frame:4"))
375 {
376 fselected = 4;
377 request.Url = request.Url.Replace("frame:4","");
378 }
379
380
381 byte[] imageBytes;
382 if (request.Url.StartsWith("data:image"))
383 {
384 imageBytes = LoadImage(request.Url);
385 if (imageBytes.Length > signArtist.Settings.MaxFileSizeInBytes )
386 {
387 //The file is too large, show a message to the player and attempt to start the next download.
388 signArtist.SendMessage(request.Sender, "FileTooLarge", imageBytes.Length, signArtist.Settings.MaxFileSizeInBytes);
389 StartNextDownload(true);
390 yield break;
391 }
392 }
393 else
394 {
395
396 if (ItemManager.itemDictionaryByName.ContainsKey(request.Url))
397 {
398 request.Url = string.Format(ItemIconUrl, request.Url);
399 }
400
401 UnityWebRequest www = UnityWebRequest.Get(request.Url);
402
403 yield return www.SendWebRequest();
404
405 // Verify that there is a valid reference to the plugin from this class.
406 if (signArtist == null)
407 {
408 throw new NullReferenceException("signArtist");
409 }
410
411 // Verify that the webrequest was succesful.
412 if (www.isNetworkError || www.isHttpError)
413 {
414 // The webrequest wasn't succesful, show a message to the player and attempt to start the next download.
415 signArtist.SendMessage(request.Sender, "WebErrorOccurred", www.error);
416 www.Dispose();
417 StartNextDownload(true);
418 yield break;
419 }
420
421
422 //Verify that the file doesn't exceed the maximum configured filesize.
423 if (www.downloadedBytes > signArtist.Settings.MaxFileSizeInBytes )
424 {
425 //The file is too large, show a message to the player and attempt to start the next download.
426 signArtist.SendMessage(request.Sender, "FileTooLarge", www.downloadedBytes, signArtist.Settings.MaxFileSizeInBytes);
427 www.Dispose();
428 StartNextDownload(true);
429 yield break;
430 }
431
432 // Get the bytes array for the image from the webrequest and lookup the target image size for the targeted sign.
433 if (request.Raw)
434 {
435 imageBytes = www.downloadHandler.data;
436 }
437 else
438 {
439 imageBytes = GetImageBytes(www);
440 }
441 www.Dispose();
442 }
443
444
445 ImageSize size = GetImageSizeFor(request.Sign);
446
447 // Verify that we have image size data for the targeted sign.
448 if (size == null)
449 {
450 // 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.
451 signArtist.SendMessage(request.Sender, "ErrorOccurred");
452 signArtist.PrintWarning($"Couldn't find the required image size for {request.Sign.PrefabName}, please report this in the plugin's thread.");
453 StartNextDownload(true);
454 //www.Dispose();
455 yield break;
456 }
457
458 RotateFlipType rotation = RotateFlipType.RotateNoneFlipNone;
459 if (request.Hor)
460 {
461 rotation = RotateFlipType.RotateNoneFlipX;
462 }
463
464 object rotateObj = Interface.Call("GetImageRotation", request.Sign.Entity);
465 if (rotateObj is RotateFlipType)
466 {
467 rotation = (RotateFlipType)rotateObj;
468 }
469
470 // Get the bytes array for the resized image for the targeted sign.
471 byte[] resizedImageBytes = imageBytes.ResizeImage(size.Width, size.Height, size.ImageWidth, size.ImageHeight, signArtist.Settings.EnforceJpeg && !request.Raw, rotation);
472 // Verify that the resized file doesn't exceed the maximum configured filesize.
473 if (resizedImageBytes.Length > signArtist.Settings.MaxFileSizeInBytes)
474 {
475 // The file is too large, show a message to the player and attempt to start the next download.
476 signArtist.SendMessage(request.Sender, "FileTooLarge", resizedImageBytes.Length, signArtist.Settings.MaxFileSizeInBytes);
477 // www.Dispose();
478 StartNextDownload(true);
479 yield break;
480 }
481
482 // Check if the sign already has a texture assigned to it.
483 if (request.Sign.TextureId() > 0)
484 {
485 // A texture was already assigned, remove this file to make room for the new one.
486 FileStorage.server.Remove(request.Sign.TextureId(), FileStorage.Type.png, request.Sign.NetId);
487 }
488
489 // Create the image on the filestorage and send out a network update for the sign.
490 request.Sign.SetImage(FileStorage.server.Store(resizedImageBytes, FileStorage.Type.png, request.Sign.NetId),fselected);
491 request.Sign.SendNetworkUpdate();
492
493 // Notify the player that the image was loaded.
494 signArtist.SendMessage(request.Sender, "ImageLoaded");
495
496 // Call the Oxide hook 'OnSignUpdated' to notify other plugins of the update event.
497 Interface.Oxide.CallHook("OnSignUpdated", request.Sign, request.Sender);
498
499 if (request.Sender != null)
500 {
501 // Check if logging to console is enabled.
502 if (signArtist.Settings.ConsoleLogging)
503 {
504 // Console logging is enabled, show a message in the server console.
505 signArtist.Puts(signArtist.GetTranslation("LogEntry"), request.Sender.displayName,
506 request.Sender.userID, request.Sign.TextureId(), request.Sign.ShortPrefabName, request.Url);
507 }
508
509 // Check if logging to file is enabled.
510 if (signArtist.Settings.FileLogging)
511 {
512 // File logging is enabled, add an entry to the logfile.
513 signArtist.LogToFile("log",
514 string.Format(signArtist.GetTranslation("LogEntry"), request.Sender.displayName,
515 request.Sender.userID, request.Sign.TextureId(), request.Sign.ShortPrefabName,
516 request.Url), signArtist);
517 }
518
519 if (signArtist.Settings.Discordlogging)
520 {
521 // Discord logging is enabled, add an entry to the logfile.
522 StartCoroutine(LogToDiscord(request));
523 }
524 }
525 // Attempt to start the next download.
526 StartNextDownload(true);
527 //www.Dispose();
528 }
529
530 private IEnumerator LogToDiscord(DownloadRequest request)
531 {
532 BasePlayer player = request.Sender;
533 IPaintableEntity sign = request.Sign;
534 var msg = DiscordMessage(ConVar.Server.hostname, player.displayName, player.UserIDString, sign.ShortPrefabName, request.Url, sign.Entity.transform.position.ToString());
535 string jsonmsg = JsonConvert.SerializeObject(msg);
536 UnityWebRequest wwwpost = new UnityWebRequest(signArtist.Settings.DiscordWebhook, "POST");
537 byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(jsonmsg.ToString());
538 wwwpost.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
539 wwwpost.SetRequestHeader("Content-Type", "application/json");
540 yield return wwwpost.SendWebRequest();
541
542 if (wwwpost.isNetworkError || wwwpost.isHttpError)
543 {
544 signArtist.PrintError(wwwpost.error);
545 signArtist.PrintError(jsonmsg);
546 yield break;
547 }
548 wwwpost.Dispose();
549 }
550
551 private Message DiscordMessage(string servername, string playername, string userid, string itemname, string imgurl, string location)
552 {
553 string steamprofile = "https://steamcommunity.com/profiles/" + userid;
554 var fields = new List<Message.Fields>()
555 {
556 new Message.Fields("Player: " + playername, $"[{userid}]({steamprofile})", true),
557 new Message.Fields("Entity", itemname, true),
558 new Message.Fields("Image Url", imgurl, false),
559 new Message.Fields("Teleport position", "teleportpos " + location.Replace(" ", string.Empty), false)
560 };
561 var footer = new Message.Footer($"Logged @{DateTime.UtcNow:dd/MM/yy HH:mm:ss}");
562 var image = new Message.Image(imgurl);
563 var embeds = new List<Message.Embeds>()
564 {
565 new Message.Embeds("Server - " + servername, "A sign has been updated" , fields, footer, image)
566 };
567 Message msg = new Message(signArtist.Settings.DiscordUsername, signArtist.Settings.AvatarUrl, embeds);
568 return msg;
569 }
570
571
572 /// <summary>
573 /// Restores the image and adds it to the sign again.
574 /// </summary>
575 /// <param name="request">The requested <see cref="RestoreRequest"/> instance. </param>
576 /// <returns></returns>
577 private IEnumerator RestoreImage(RestoreRequest request)
578 {
579 // Verify that there is a valid reference to the plugin from this class.
580 if (signArtist == null)
581 {
582 throw new NullReferenceException("signArtist");
583 }
584
585 byte[] imageBytes;
586
587 // Check if the sign already has a texture assigned to it.
588 if (request.Sign.TextureId() == 0)
589 {
590 // No texture was previously assigned, show a message to the player.
591 signArtist.SendMessage(request.Sender, "RestoreErrorOccurred");
592 StartNextRestore(true);
593
594 yield break;
595 }
596
597 // Cache the byte array of the currently stored file.
598 imageBytes = FileStorage.server.Get(request.Sign.TextureId(), FileStorage.Type.png, request.Sign.NetId);
599 ImageSize size = GetImageSizeFor(request.Sign);
600
601 // Verify that we have image size data for the targeted sign.
602 if (size == null)
603 {
604 // 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.
605 signArtist.SendMessage(request.Sender, "ErrorOccurred");
606 signArtist.PrintWarning($"Couldn't find the required image size for {request.Sign.PrefabName}, please report this in the plugin's thread.");
607 StartNextRestore(true);
608
609 yield break;
610 }
611
612 // Remove the texture from the FileStorage.
613 FileStorage.server.Remove(request.Sign.TextureId(), FileStorage.Type.png, request.Sign.NetId);
614
615 // Get the bytes array for the resized image for the targeted sign.
616 byte[] resizedImageBytes = imageBytes.ResizeImage(size.Width, size.Height, size.ImageWidth, size.ImageHeight, signArtist.Settings.EnforceJpeg && !request.Raw);
617
618 // Create the image on the filestorage and send out a network update for the sign.
619 request.Sign.SetImage(FileStorage.server.Store(resizedImageBytes, FileStorage.Type.png, request.Sign.NetId),0);
620 request.Sign.SendNetworkUpdate();
621
622 // Notify the player that the image was loaded.
623 signArtist.SendMessage(request.Sender, "ImageRestored");
624
625 // Call the Oxide hook 'OnSignUpdated' to notify other plugins of the update event.
626 Interface.Oxide.CallHook("OnSignUpdated", request.Sign, request.Sender);
627
628 // Attempt to start the next download.
629 StartNextRestore(true);
630 }
631
632 /// <summary>
633 /// Gets the target image size for a <see cref="Signage"/>.
634 /// </summary>
635 /// <param name="signage"></param>
636 private ImageSize GetImageSizeFor(IPaintableEntity signage)
637 {
638 if (signArtist.ImageSizePerAsset.ContainsKey(signage.ShortPrefabName))
639 {
640 return signArtist.ImageSizePerAsset[signage.ShortPrefabName];
641 }
642
643 return null;
644 }
645
646 /// <summary>
647 /// Converts the <see cref="Texture2D"/> from the webrequest to a <see cref="byte"/> array.
648 /// </summary>
649 /// <param name="www">The completed webrequest. </param>
650 private byte[] GetImageBytes(UnityWebRequest www)
651 {
652 Texture2D texture = new Texture2D(2, 2);
653 texture.LoadImage(www.downloadHandler.data);
654
655 byte[] image;
656
657 if (texture.format == TextureFormat.ARGB32 && !signArtist.Settings.EnforceJpeg)
658 {
659 image = texture.EncodeToPNG();
660 }
661 else
662 {
663 image = texture.EncodeToJPG(signArtist.Settings.Quality);
664 }
665
666 DestroyImmediate(texture);
667
668 return image;
669 }
670 }
671
672 #endregion Image Download Behaviour
673 private interface IBasePaintableEntity
674 {
675 BaseEntity Entity { get; }
676 string PrefabName { get; }
677 string ShortPrefabName { get; }
678 uint NetId { get; }
679 void SendNetworkUpdate();
680 }
681
682 private interface IPaintableEntity : IBasePaintableEntity
683 {
684 void SetImage(uint id,int frameid);
685 bool CanUpdate(BasePlayer player);
686 uint TextureId();
687 }
688
689 private class BasePaintableEntity : IBasePaintableEntity
690 {
691 public BaseEntity Entity { get; }
692 public string PrefabName { get; }
693 public string ShortPrefabName { get; }
694 public uint NetId { get; }
695
696 protected BasePaintableEntity(BaseEntity entity)
697 {
698 Entity = entity;
699 PrefabName = Entity.PrefabName;
700 ShortPrefabName = Entity.ShortPrefabName;
701 NetId = Entity.net.ID;
702 }
703
704 public void SendNetworkUpdate()
705 {
706 Entity.SendNetworkUpdate();
707 }
708 }
709
710 private class PaintableSignage : BasePaintableEntity, IPaintableEntity
711 {
712 public Signage Sign { get; set; }
713
714 public PaintableSignage(Signage sign) : base(sign)
715 {
716 Sign = sign;
717 }
718
719 public void SetImage(uint id,int frameid)
720 {
721 //Sign.textureIDs = new uint[] { id };
722 Sign.textureIDs[frameid] = id;
723 }
724
725 public bool CanUpdate(BasePlayer player)
726 {
727 return Sign.CanUpdateSign(player);
728 }
729
730 public uint TextureId()
731 {
732 return Sign.textureIDs.First();
733 }
734 }
735
736 private class PaintableFrame : BasePaintableEntity, IPaintableEntity
737 {
738 public PhotoFrame Sign { get; set; }
739
740 public PaintableFrame(PhotoFrame sign) : base(sign)
741 {
742 Sign = sign;
743 }
744
745 public void SetImage(uint id,int frameid)
746 {
747 Sign._overlayTextureCrc = id;
748 }
749
750 public bool CanUpdate(BasePlayer player)
751 {
752 return Sign.CanUpdateSign(player);
753 }
754
755 public uint TextureId()
756 {
757 return Sign._overlayTextureCrc;
758 }
759 }
760
761
762 #region Init
763 /// <summary>
764 /// Oxide hook that is triggered when the plugin is loaded.
765 /// </summary>
766 ///
767 private void Init()
768 {
769 // Register all the permissions used by the plugin
770 permission.RegisterPermission("signartist.file", this);
771 permission.RegisterPermission("signartist.ignorecd", this);
772 permission.RegisterPermission("signartist.ignoreowner", this);
773 permission.RegisterPermission("signartist.raw", this);
774 permission.RegisterPermission("signartist.restore", this);
775 permission.RegisterPermission("signartist.restoreall", this);
776 permission.RegisterPermission("signartist.text", this);
777 permission.RegisterPermission("signartist.url", this);
778
779 AddCovalenceCommand("sil", "SilCommand");
780 AddCovalenceCommand("silt", "SiltCommand");
781 AddCovalenceCommand("sili", "SilItemCommand");
782 AddCovalenceCommand("silrestore", "RestoreCommand");
783
784 // Initialize the dictionary with all paintable object assets and their target sizes
785 ImageSizePerAsset = new Dictionary<string, ImageSize>
786 {
787 // Picture Frames
788 ["sign.pictureframe.landscape"] = new ImageSize(256, 128), // Landscape Picture Frame
789 ["sign.pictureframe.portrait"] = new ImageSize(128, 256), // Portrait Picture Frame
790 ["sign.pictureframe.tall"] = new ImageSize(128, 512), // Tall Picture Frame
791 ["sign.pictureframe.xl"] = new ImageSize(512, 512), // XL Picture Frame
792 ["sign.pictureframe.xxl"] = new ImageSize(1024, 512), // XXL Picture Frame
793
794 // Wooden Signs
795 ["sign.small.wood"] = new ImageSize(128, 64), // Small Wooden Sign
796 ["sign.medium.wood"] = new ImageSize(256, 128), // Wooden Sign
797 ["sign.large.wood"] = new ImageSize(256, 128), // Large Wooden Sign
798 ["sign.huge.wood"] = new ImageSize(512, 128), // Huge Wooden Sign
799
800 // Banners
801 ["sign.hanging.banner.large"] = new ImageSize(64, 256), // Large Banner Hanging
802 ["sign.pole.banner.large"] = new ImageSize(64, 256), // Large Banner on Pole
803
804 // Hanging Signs
805 ["sign.hanging"] = new ImageSize(128, 256), // Two Sided Hanging Sign
806 ["sign.hanging.ornate"] = new ImageSize(256, 128), // Two Sided Ornate Hanging Sign
807
808 // Town Signs
809 ["sign.post.single"] = new ImageSize(128, 64), // Single Sign Post
810 ["sign.post.double"] = new ImageSize(256, 256), // Double Sign Post
811 ["sign.post.town"] = new ImageSize(256, 128), // One Sided Town Sign Post
812 ["sign.post.town.roof"] = new ImageSize(256, 128), // Two Sided Town Sign Post
813
814 ["photoframe.large"] = new ImageSize(320, 240),
815 ["photoframe.portrait"] = new ImageSize(320, 384),
816 ["photoframe.landscape"] = new ImageSize(320, 240),
817
818
819 // Other paintable assets
820 ["sign.neon.xl.animated"] = new ImageSize(256, 256),
821 ["sign.neon.xl"] = new ImageSize(256, 256),
822 ["sign.neon.125x215.animated"] = new ImageSize(128, 256),
823 ["sign.neon.125x215"] = new ImageSize(128, 256),
824 ["sign.neon.125x125"] = new ImageSize(128, 128),
825 ["spinner.wheel.deployed"] = new ImageSize(512, 512, 285, 285), // Spinning Wheel
826 };
827 }
828
829 private void GetSteamworksImages()
830 {
831 foreach (InventoryDef item in Steamworks.SteamInventory.Definitions)
832 {
833 string shortname = item.GetProperty("itemshortname");
834 if (item == null || string.IsNullOrEmpty(shortname))
835 continue;
836
837 if (item.Id < 100)
838 continue;
839
840 ulong workshopid;
841 if (!ulong.TryParse(item.GetProperty("workshopid"), out workshopid))
842 continue;
843
844 if (string.IsNullOrEmpty(item.IconUrl)) continue;
845 SkiniconUrls[workshopid] = item.IconUrl;
846 }
847 }
848
849 /// <summary>
850 /// Oxide hook that is triggered to automatically load the configuration file.
851 /// </summary>
852 protected override void LoadConfig()
853 {
854 base.LoadConfig();
855 Settings = Config.ReadObject<SignArtistConfig>();
856 SaveConfig();
857 }
858
859 /// <summary>
860 /// Oxide hook that is triggered to automatically load the default configuration file when no file exists.
861 /// </summary>
862 protected override void LoadDefaultConfig()
863 {
864 Settings = SignArtistConfig.DefaultConfig();
865 }
866
867 /// <summary>
868 /// Oxide hook that is triggered to save the configuration file.
869 /// </summary>
870 protected override void SaveConfig()
871 {
872 Config.WriteObject(Settings);
873 }
874
875 /// <summary>
876 /// Oxide hook that is triggered when the server has fully initialized.
877 /// </summary>
878 private void OnServerInitialized()
879 {
880 // Create a new GameObject and attach the UnityEngine script to it for handling the image downloads.
881 imageDownloaderGameObject = new GameObject("ImageDownloader");
882 imageDownloader = imageDownloaderGameObject.AddComponent<ImageDownloader>();
883 if ((Steamworks.SteamInventory.Definitions?.Length ?? 0) == 0)
884 {
885 PrintWarning("Waiting for Steamworks to update item definitions....");
886 Steamworks.SteamInventory.OnDefinitionsUpdated += GetSteamworksImages;
887 }
888 else GetSteamworksImages();
889 }
890
891 /// <summary>
892 /// Oxide hook that is triggered when the plugin is unloaded.
893 /// </summary>
894 private void Unload()
895 {
896 // Destroy the created GameObject and cleanup.
897 UnityEngine.Object.Destroy(imageDownloaderGameObject);
898 imageDownloader = null;
899 cooldowns = null;
900
901 Steamworks.SteamInventory.OnDefinitionsUpdated -= GetSteamworksImages;
902 }
903
904 /// <summary>
905 /// Handles the /sil command.
906 /// </summary>
907 /// <param name="iplayer">The player that has executed the command. </param>
908 /// <param name="command">The name of the command that was executed. </param>
909 /// <param name="args">All arguments that were passed with the command. </param>
910 ///
911 #endregion Init
912
913 #region Localization
914 /// <summary>
915 /// Oxide hook that is triggered automatically after it has been loaded to initialize the messages for the Lang API.
916 /// </summary>
917 protected override void LoadDefaultMessages()
918 {
919 // Register all messages used by the plugin in the Lang API.
920 lang.RegisterMessages(new Dictionary<string, string>
921 {
922 // Messages used throughout the plugin.
923 ["WebErrorOccurred"] = "Failed to download the image! Error {0}.",
924 ["FileTooLarge"] = "The file {0}Bytes exceeds the maximum file size of {1}Bytes.",
925 ["ErrorOccurred"] = "An unknown error has occured, if this error keeps occuring please notify the server admin.",
926 ["RestoreErrorOccurred"] = "Can't restore the sign because no texture is assigned to it.",
927 ["DownloadQueued"] = "Your image was added to the download queue!",
928 ["RestoreQueued"] = "Your sign was added to the restore queue!",
929 ["RestoreBatchQueued"] = "You added all {0} signs to the restore queue!",
930 ["ImageLoaded"] = "The image was succesfully loaded to the sign!",
931 ["ImageRestored"] = "The image was succesfully restored for the sign!",
932 ["LogEntry"] = "Player `{0}` (SteamId: {1}) loaded {2} into {3} from {4}",
933 ["NoSignFound"] = "Unable to find a sign! Make sure you are looking at one and that you are not too far away from it.",
934 ["Cooldown"] = "You can't use the command yet! Remaining cooldown: {0}.",
935 ["SignNotOwned"] = "You can't change this sign as it is protected by a tool cupboard.",
936 ["NoItemHeld"] = "You're not holding an item.",
937 ["ActionQueuedAlready"] = "An action has already been queued for this sign, please wait for this action to complete.",
938 ["SyntaxSilCommand"] = "Syntax error!\nSyntax: /sil <url> [raw]",
939 ["SyntaxSiltCommand"] = "Syntax error!\nSyntax: /silt <message> [<fontsize:number>] [<color:hex value>] [<bgcolor:hex value>] [raw]",
940 ["NoPermission"] = "You don't have permission to use this command.",
941 ["NoPermissionFile"] = "You don't have permission to use images from the server's filesystem.",
942 ["NoPermissionRaw"] = "You don't have permission to use raw images, loading normally instead.",
943 ["NoPermissionRestoreAll"] = "You don't have permission to use restore all signs at once.",
944
945 // Cooldown formatting 'translations'.
946 ["day"] = "day",
947 ["days"] = "days",
948 ["hour"] = "hour",
949 ["hours"] = "hours",
950 ["minute"] = "minute",
951 ["minutes"] = "minutes",
952 ["second"] = "second",
953 ["seconds"] = "seconds",
954 ["and"] = "and"
955 }, this);
956 }
957 #endregion Localization
958
959 #region Commands
960 [Command("sil"), Permission("signartist.url")]
961 private void SilCommand(IPlayer iplayer, string command, string[] args)
962 {
963 var player = iplayer.Object as BasePlayer;
964 // Verify if the correct syntax is used.
965 if (args.Length < 1)
966 {
967 // Invalid syntax was used, show an error message to the player.
968 SendMessage(player, "SyntaxSilCommand");
969
970 return;
971 }
972
973 // Verify if the player has permission to use this command.
974 if (!HasPermission(player, "signartist.url"))
975 {
976 // The player doesn't have permission to use this command, show an error message.
977 SendMessage(player, "NoPermission");
978
979 return;
980 }
981
982 // Verify that the command isn't on cooldown for the user.
983 if (HasCooldown(player))
984 {
985 // The command is still on cooldown for the player, show an error message.
986 SendMessage(player, "Cooldown", FormatCooldown(GetCooldown(player)));
987
988 return;
989 }
990
991 // Check if the player is looking at a sign.
992 IPaintableEntity sign;
993 if (!IsLookingAtSign(player, out sign))
994 {
995 // The player isn't looking at a sign or is too far away from it, show an error message.
996 SendMessage(player, "NoSignFound");
997
998 return;
999 }
1000
1001 // Check if the player is able to update the sign.
1002 if (!CanChangeSign(player, sign))
1003 {
1004 // The player isn't able to update the sign, show an error message.
1005 SendMessage(player, "SignNotOwned");
1006
1007 return;
1008 }
1009
1010 // Check if the player wants to add the image from the server's filesystem and has the permission to do so.
1011 if (args[0].StartsWith("file://") && !HasPermission(player, "signartist.file"))
1012 {
1013 // The player doesn't have permission for this, show an error message.
1014 SendMessage(player, "NoPermissionFile");
1015
1016 return;
1017 }
1018
1019 if (args[0].StartsWith("i:"))
1020 {
1021 args[0] = args[0].Replace("i:",@"file://f://Rust//images//");
1022 }
1023
1024 // Check if the player wants to add the image as a raw image and has the permission to do so.
1025 bool raw = args.Length > 1 && args[1].Equals("raw", StringComparison.OrdinalIgnoreCase);
1026 if (raw && !HasPermission(player, "signartist.raw"))
1027 {
1028 // The player doesn't have permission for this, show a message and disable raw.
1029 SendMessage(player, "NoPermissionRaw");
1030 raw = false;
1031 }
1032
1033 // This sign pastes in reverse, so we'll check and set a var to flip it
1034 bool hor = sign.ShortPrefabName == "sign.hanging";
1035
1036 // Notify the player that it is added to the queue.
1037 SendMessage(player, "DownloadQueued");
1038
1039 // Queue the download of the specified image.
1040 imageDownloader.QueueDownload(args[0], player, sign, raw, hor);
1041
1042 // Call external hook
1043 Interface.Oxide.CallHook("OnImagePost", player, args[0]);
1044
1045 // Set the cooldown on the command for the player if the cooldown setting is enabled.
1046 SetCooldown(player);
1047 }
1048
1049 private static byte[] LoadImage(string data)
1050 {
1051 //data:image/gif;base64,
1052 //data:image/jpeg;base64,
1053 //data:image/png;base64,
1054 data = data.Replace("data:image/gif;base64,", "");
1055 data = data.Replace("data:image/jpeg;base64,", "");
1056 data = data.Replace("data:image/png;base64,", "");
1057 //byte[] bytes = Convert.FromBase64String(data);
1058 //System.Random random = new System.Random();
1059 //string filePath = "F://Rust/images//" + (random.Next(123456, 66666666)).ToString() + ".jpg";
1060 //System.IO.File.WriteAllBytes(filePath, Convert.FromBase64String(data));
1061 return Convert.FromBase64String(data);
1062 }
1063
1064 [Command("sili"), Permission("signartist.url")]
1065 private void SilItemCommand(IPlayer iplayer, string command, string[] args)
1066 {
1067 var player = iplayer.Object as BasePlayer;
1068 if (!HasPermission(player, "signartist.url"))
1069 {
1070 SendMessage(player, "NoPermission");
1071 return;
1072 }
1073
1074 if (HasCooldown(player))
1075 {
1076 SendMessage(player, "Cooldown", FormatCooldown(GetCooldown(player)));
1077 return;
1078 }
1079
1080 IPaintableEntity sign;
1081 if (!IsLookingAtSign(player, out sign))
1082 {
1083 SendMessage(player, "NoSignFound");
1084 return;
1085 }
1086
1087 if (!CanChangeSign(player, sign))
1088 {
1089 SendMessage(player, "SignNotOwned");
1090 return;
1091 }
1092
1093 Item held = player.GetActiveItem();
1094 if (held == null)
1095 {
1096 SendMessage(player, "NoItemHeld");
1097 return;
1098 }
1099
1100 string shortname = held.info.shortname;
1101
1102 bool hor = sign.ShortPrefabName == "sign.hanging";
1103
1104 SendMessage(player, "DownloadQueued");
1105 bool defaultskin = false;
1106 if (args.Length == 1 && args[0] == "default") defaultskin = true;
1107 if (held.skin != 0uL && !defaultskin)
1108 {
1109 string url;
1110 if (SkiniconUrls.TryGetValue(held.skin, out url))
1111 {
1112 shortname = url;
1113 }
1114 else
1115 {
1116 ServerMgr.Instance.StartCoroutine(DownloadWorkshopskin(held, sign, hor));
1117 return;
1118 }
1119 }
1120
1121 imageDownloader.QueueDownload(shortname, player, sign, false, hor);
1122
1123 Interface.Oxide.CallHook("OnImagePost", player, shortname);
1124
1125 SetCooldown(player);
1126 }
1127
1128 private const string FindWorkshopSkinUrl = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/";
1129
1130 private IEnumerator DownloadWorkshopskin(Item held, IPaintableEntity sign, bool hor)
1131 {
1132 BasePlayer player = held.GetOwnerPlayer();
1133 WWWForm form = new WWWForm();
1134 form.AddField("itemcount", "1");
1135 form.AddField("publishedfileids[0]", held.skin.ToString());
1136 UnityWebRequest www = UnityWebRequest.Post(FindWorkshopSkinUrl, form);
1137 yield return www.SendWebRequest();
1138 string url = "";
1139 // Verify that the webrequest was succesful.
1140 if (www.isNetworkError || www.isHttpError)
1141 {
1142 // The webrequest wasn't succesful, show a message to the player and attempt to start the next download.
1143 PrintError(www.error.ToString());
1144 url = held.info.shortname;
1145 }
1146 var json = JsonConvert.DeserializeObject<GetPublishedFileDetailsClass>(www.downloadHandler.text);
1147 url = json.response.publishedfiledetails[0].preview_url;
1148 imageDownloader.QueueDownload(url, player, sign, false, hor);
1149
1150 Interface.Oxide.CallHook("OnImagePost", player, held.info.shortname);
1151
1152 SetCooldown(player);
1153 }
1154
1155 /// <summary>
1156 /// Handles the /silt command
1157 /// </summary>
1158 /// <param name="iplayer">The player that has executed the command. </param>
1159 /// <param name="command">The name of the command that was executed. </param>
1160 /// <param name="args">All arguments that were passed with the command. </param>
1161 [Command("silt"), Permission("signartist.text")]
1162 private void SiltCommand(IPlayer iplayer, string command, string[] args)
1163 {
1164 var player = iplayer.Object as BasePlayer;
1165 // Verify if the correct syntax is used.
1166 if (args.Length < 1)
1167 {
1168 // Invalid syntax was used, show an error message to the player.
1169 SendMessage(player, "SyntaxSiltCommand");
1170
1171 return;
1172 }
1173
1174 // Verify if the player has permission to use this command.
1175 if (!HasPermission(player, "signartist.text"))
1176 {
1177 // The player doesn't have permission to use this command, show an error message.
1178 SendMessage(player, "NoPermission");
1179
1180 return;
1181 }
1182
1183 // Verify that the command isn't on cooldown for the user.
1184 if (HasCooldown(player))
1185 {
1186 // The command is still on cooldown for the player, show an error message.
1187 SendMessage(player, "Cooldown", FormatCooldown(GetCooldown(player)));
1188
1189 return;
1190 }
1191
1192 // Check if the player is looking at a sign.
1193 IPaintableEntity sign;
1194 if (!IsLookingAtSign(player, out sign))
1195 {
1196 // The player isn't looking at a sign or is too far away from it, show an error message.
1197 SendMessage(player, "NoSignFound");
1198
1199 return;
1200 }
1201
1202 // Check if the player is able to update the sign.
1203 if (!CanChangeSign(player, sign))
1204 {
1205 // The player isn't able to update the sign, show an error message.
1206 SendMessage(player, "SignNotOwned");
1207
1208 return;
1209 }
1210
1211 // Build the URL for the /silt command
1212 string message = args[0].EscapeForUrl();
1213 int fontsize = 80;
1214 string color = "000";
1215 string bgcolor = "0FFF";
1216 string format = "png32";
1217
1218 // Replace the default fontsize if the player specified one.
1219 if (args.Length > 1)
1220 {
1221 int.TryParse(args[1], out fontsize);
1222 }
1223
1224 // Replace the default color if the player specified one.
1225 if (args.Length > 2)
1226 {
1227 color = args[2].Trim(' ', '#');
1228 }
1229
1230 // Replace the default color if the player specified one.
1231 if (args.Length > 3)
1232 {
1233 bgcolor = args[3].Trim(' ', '#');
1234 }
1235
1236 // Check if the player wants to add the image as a raw image and has the permission to do so.
1237 bool raw = args.Length > 4 && args[4].Equals("raw", StringComparison.OrdinalIgnoreCase);
1238 if (raw && !HasPermission(player, "signartist.raw"))
1239 {
1240 // The player doesn't have permission for this, show a message and disable raw.
1241 SendMessage(player, "NoPermissionRaw");
1242 raw = false;
1243 }
1244
1245 // Correct the format if required
1246 if (Settings.EnforceJpeg)
1247 {
1248 format = "jpg";
1249 }
1250
1251 // Get the size for the image
1252 ImageSize size = null;
1253 if (ImageSizePerAsset.ContainsKey(sign.ShortPrefabName))
1254 {
1255 size = ImageSizePerAsset[sign.ShortPrefabName];
1256 }
1257
1258 // Verify that we have image size data for the targeted sign.
1259 if (size == null)
1260 {
1261 // 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.
1262 SendMessage(player, "ErrorOccurred");
1263 PrintWarning($"Couldn't find the required image size for {sign.PrefabName}, please report this in the plugin's thread.");
1264
1265 return;
1266 }
1267
1268 // Combine all the values into the url;
1269 string url = $"http://assets.imgix.net/~text?fm={format}&txtalign=middle,center&txtsize={fontsize}&txt={message}&w={size.ImageWidth}&h={size.ImageHeight}&txtclr={color}&bg={bgcolor}";
1270
1271 // Notify the player that it is added to the queue.
1272 SendMessage(player, "DownloadQueued");
1273
1274 // This sign pastes in reverse, so we'll check and set a var to flip it
1275 bool hor = sign.ShortPrefabName == "sign.hanging";
1276
1277 // Queue the download of the specified image.
1278 imageDownloader.QueueDownload(url, player, sign, raw, hor);
1279
1280 // Call external hook
1281 Interface.Oxide.CallHook("OnImagePost", player, url);
1282
1283 // Set the cooldown on the command for the player if the cooldown setting is enabled.
1284 SetCooldown(player);
1285 }
1286
1287 /// <summary>
1288 /// Handles the /silrestore command
1289 /// </summary>
1290 /// <param name="iplayer">The player that has executed the command. </param>
1291 /// <param name="command">The name of the command that was executed. </param>
1292 /// <param name="args">All arguments that were passed with the command. </param>
1293 [Command("silrestore"), Permission("signartist.raw")]
1294 private void RestoreCommand(IPlayer iplayer, string command, string[] args)
1295 {
1296 var player = iplayer.Object as BasePlayer;
1297 // Verify if the player has permission to use this command.
1298 if (!HasPermission(player, "signartist.restore"))
1299 {
1300 // The player doesn't have permission to use this command, show an error message.
1301 SendMessage(player, "NoPermission");
1302
1303 return;
1304 }
1305
1306 // Check if the user wants to restore the sign or signs as raw images and has the permission to do so
1307 bool raw = string.IsNullOrEmpty(args.FirstOrDefault(s => s.Equals("raw", StringComparison.OrdinalIgnoreCase)));
1308 if (raw && !HasPermission(player, "signartist.raw"))
1309 {
1310 // The player doesn't have permission for this, show a message and disable raw.
1311 SendMessage(player, "NoPermissionRaw");
1312 raw = false;
1313 }
1314
1315 // Check if the user wants to restore all signs and has the permission to do so.
1316 bool all = args.Any(s => s.Equals("all", StringComparison.OrdinalIgnoreCase));
1317 if (all && !HasPermission(player, "signartist.restoreall"))
1318 {
1319 // The player doesn't have permission for this, show a message and disable raw.
1320 SendMessage(player, "NoPermissionRestoreAll");
1321
1322 return;
1323 }
1324
1325 // Check if the player is looking at a sign if not all signs should be restored.
1326 if (!all)
1327 {
1328 IPaintableEntity sign;
1329 if (!IsLookingAtSign(player, out sign))
1330 {
1331 // The player isn't looking at a sign or is too far away from it, show an error message.
1332 SendMessage(player, "NoSignFound");
1333
1334 return;
1335 }
1336
1337 // Notify the player that it is added to the queue.
1338 SendMessage(player, "RestoreQueued");
1339
1340 // Queue the restore of the image on the specified sign.
1341 imageDownloader.QueueRestore(player, sign, raw);
1342
1343 return;
1344 }
1345
1346 // The player wants to restore all signs.
1347 Signage[] allSigns = UnityEngine.Object.FindObjectsOfType<Signage>();
1348
1349 // Notify the player that they were added to the queue
1350 SendMessage(player, "RestoreBatchQueued", allSigns.Length);
1351
1352 // Queue every sign to be restored.
1353 foreach (Signage sign in allSigns)
1354 {
1355 imageDownloader.QueueRestore(player, new PaintableSignage(sign), raw);
1356 }
1357 }
1358
1359 #endregion Commands
1360
1361 #region Methods
1362 /// <summary>
1363 /// Check if the given <see cref="BasePlayer"/> is able to use the command.
1364 /// </summary>
1365 /// <param name="player">The player to check. </param>
1366 private bool HasCooldown(BasePlayer player)
1367 {
1368 // Check if cooldown is enabled.
1369 if (Settings.Cooldown <= 0)
1370 {
1371 return false;
1372 }
1373
1374 // Check if cooldown is ignored for the player.
1375 if (HasPermission(player, "signartist.ignorecd"))
1376 {
1377 return false;
1378 }
1379
1380 // Make sure there is an entry for the player in the dictionary.
1381 if (!cooldowns.ContainsKey(player.userID))
1382 {
1383 cooldowns.Add(player.userID, 0);
1384 }
1385
1386 // Check if the command is on cooldown or not.
1387 return Time.realtimeSinceStartup - cooldowns[player.userID] < Settings.Cooldown;
1388 }
1389
1390 /// <summary>
1391 /// Returns the cooldown in seconds for the given <see cref="BasePlayer"/>.
1392 /// </summary>
1393 /// <param name="player">The player to obtain the cooldown of. </param>
1394 private float GetCooldown(BasePlayer player)
1395 {
1396 return Settings.Cooldown - (Time.realtimeSinceStartup - cooldowns[player.userID]);
1397 }
1398
1399 /// <summary>
1400 /// Sets the last use for the cooldown handling of the command for the given <see cref="BasePlayer"/>.
1401 /// </summary>
1402 /// <param name="player">The player to put the command on cooldown for. </param>
1403 private void SetCooldown(BasePlayer player)
1404 {
1405 // Check if cooldown is enabled.
1406 if (Settings.Cooldown <= 0)
1407 {
1408 return;
1409 }
1410
1411 // Check if cooldown is ignored for the player.
1412 if (HasPermission(player, "signartist.ignorecd"))
1413 {
1414 return;
1415 }
1416
1417 // Make sure there is an entry for the player in the dictionary.
1418 if (!cooldowns.ContainsKey(player.userID))
1419 {
1420 cooldowns.Add(player.userID, 0);
1421 }
1422
1423 // Set the last use
1424 cooldowns[player.userID] = Time.realtimeSinceStartup;
1425 }
1426
1427 /// <summary>
1428 /// Returns a formatted string for the given cooldown.
1429 /// </summary>
1430 /// <param name="seconds">The cooldown in seconds. </param>
1431 private string FormatCooldown(float seconds)
1432 {
1433 // Create a new TimeSpan from the remaining cooldown.
1434 TimeSpan t = TimeSpan.FromSeconds(seconds);
1435 List<string> output = new List<string>();
1436
1437 // Check if it is more than a single day and add it to the result.
1438 if (t.Days >= 1)
1439 {
1440 output.Add($"{t.Days} {(t.Days > 1 ? "days" : "day")}");
1441 }
1442
1443 // Check if it is more than an hour and add it to the result.
1444 if (t.Hours >= 1)
1445 {
1446 output.Add($"{t.Hours} {(t.Hours > 1 ? "hours" : "hour")}");
1447 }
1448
1449 // Check if it is more than a minute and add it to the result.
1450 if (t.Minutes >= 1)
1451 {
1452 output.Add($"{t.Minutes} {(t.Minutes > 1 ? "minutes" : "minute")}");
1453 }
1454
1455 // Check if there is more than a second and add it to the result.
1456 if (t.Seconds >= 1)
1457 {
1458 output.Add($"{t.Seconds} {(t.Seconds > 1 ? "seconds" : "second")}");
1459 }
1460
1461 // Format the result and return it.
1462 return output.Count >= 3 ? output.ToSentence().Replace(" and", ", and") : output.ToSentence();
1463 }
1464
1465 /// <summary>
1466 /// Checks if the <see cref="BasePlayer"/> is looking at a valid <see cref="Signage"/> object.
1467 /// </summary>
1468 /// <param name="player">The player to check. </param>
1469 /// <param name="sign">When this method returns, contains the <see cref="Signage"/> the player contained in <paramref name="player" /> is looking at, or null if the player isn't looking at a sign. </param>
1470 private bool IsLookingAtSign(BasePlayer player, out IPaintableEntity sign)
1471 {
1472 RaycastHit hit;
1473 sign = null;
1474
1475 // Get the object that is in front of the player within the maximum distance set in the config.
1476 //if (Physics.Raycast(player.eyes.HeadRay(), out hit))//, Settings.MaxDistance))
1477 if (Physics.Raycast(player.eyes.HeadRay(), out hit, Settings.MaxDistance))
1478 {
1479 // Attempt to grab the Signage entity, if there is none this will set the sign to null,
1480 // otherwise this will set it to the sign the player is looking at.
1481 BaseEntity entity = hit.GetEntity();
1482 if (entity is Signage)
1483 {
1484 sign = new PaintableSignage(entity as Signage);
1485 }
1486 else if (entity is PhotoFrame)
1487 {
1488 sign = new PaintableFrame(entity as PhotoFrame);
1489 }
1490 }
1491
1492 // Return true or false depending on if we found a sign.
1493 return sign != null;
1494 }
1495
1496 /// <summary>
1497 /// Checks if the <see cref="BasePlayer"/> is allowed to change the drawing on the <see cref="Signage"/> object.
1498 /// </summary>
1499 /// <param name="player">The player to check. </param>
1500 /// <param name="sign">The sign to check. </param>
1501 /// <returns></returns>
1502 private bool CanChangeSign(BasePlayer player, IPaintableEntity sign)
1503 {
1504 return sign.CanUpdate(player) || HasPermission(player, "signartist.ignoreowner");
1505 }
1506
1507 /// <summary>
1508 /// Checks if the given <see cref="BasePlayer"/> has the specified permission.
1509 /// </summary>
1510 /// <param name="player">The player to check a permission on. </param>
1511 /// <param name="perm">The permission to check for. </param>
1512 private bool HasPermission(BasePlayer player, string perm)
1513 {
1514 return permission.UserHasPermission(player.UserIDString, perm);
1515 }
1516
1517 /// <summary>
1518 /// Send a formatted message to a single player.
1519 /// </summary>
1520 /// <param name="player">The player to send the message to. </param>
1521 /// <param name="key">The key of the message from the Lang API to get the message for. </param>
1522 /// <param name="args">Any amount of arguments to add to the message. </param>
1523 private void SendMessage(BasePlayer player, string key, params object[] args)
1524 {
1525 if (player == null) return;
1526 player.ChatMessage(string.Format(GetTranslation(key, player), args));
1527 }
1528
1529 /// <summary>
1530 /// Gets the message for a specific player from the Lang API.
1531 /// </summary>
1532 /// <param name="key">The key of the message from the Lang API to get the message for. </param>
1533 /// <param name="player">The player to get the message for. </param>
1534 /// <returns></returns>
1535 private string GetTranslation(string key, BasePlayer player = null)
1536 {
1537 return lang.GetMessage(key, this, player?.UserIDString);
1538 }
1539 #endregion Methods
1540
1541 #region Steam Workshop API Class
1542
1543 public class GetPublishedFileDetailsClass
1544 {
1545 public Response response { get; set; }
1546 }
1547
1548 public class Response
1549 {
1550 public int result { get; set; }
1551 public int resultcount { get; set; }
1552 public Publishedfiledetail[] publishedfiledetails { get; set; }
1553 }
1554
1555 public class Publishedfiledetail
1556 {
1557 public string publishedfileid { get; set; }
1558 public int result { get; set; }
1559 public string creator { get; set; }
1560 public int creator_app_id { get; set; }
1561 public int consumer_app_id { get; set; }
1562 public string filename { get; set; }
1563 public int file_size { get; set; }
1564 public string preview_url { get; set; }
1565 public string hcontent_preview { get; set; }
1566 public string title { get; set; }
1567 public string description { get; set; }
1568 public int time_created { get; set; }
1569 public int time_updated { get; set; }
1570 public int visibility { get; set; }
1571 public int banned { get; set; }
1572 public string ban_reason { get; set; }
1573 public int subscriptions { get; set; }
1574 public int favorited { get; set; }
1575 public int lifetime_subscriptions { get; set; }
1576 public int lifetime_favorited { get; set; }
1577 public int views { get; set; }
1578 public Tag[] tags { get; set; }
1579 }
1580
1581 public class Tag
1582 {
1583 public string tag { get; set; }
1584 }
1585
1586 #endregion Steam Workshop API Class
1587
1588 #region Discord Class
1589 public class Message
1590 {
1591 public string username { get; set; }
1592 public string avatar_url { get; set; }
1593 public List<Embeds> embeds { get; set; }
1594
1595 public class Fields
1596 {
1597 public string name { get; set; }
1598 public string value { get; set; }
1599 public bool inline { get; set; }
1600 public Fields(string name, string value, bool inline)
1601 {
1602 this.name = name;
1603 this.value = value;
1604 this.inline = inline;
1605 }
1606 }
1607
1608 public class Footer
1609 {
1610 public string text { get; set; }
1611 public Footer(string text)
1612 {
1613 this.text = text;
1614 }
1615 }
1616
1617 public class Image
1618 {
1619 public string url { get; set; }
1620 public Image(string url)
1621 {
1622 this.url = url;
1623 }
1624 }
1625
1626 public class Embeds
1627 {
1628 public string title { get; set; }
1629 public string description { get; set; }
1630 public Image image { get; set; }
1631 public List<Fields> fields { get; set; }
1632 public Footer footer { get; set; }
1633 public Embeds(string title, string description, List<Fields> fields, Footer footer, Image image)
1634 {
1635 this.title = title;
1636 this.description = description;
1637 this.image = image;
1638 this.fields = fields;
1639 this.footer = footer;
1640 }
1641 }
1642
1643 public Message(string username, string avatar_url, List<Embeds> embeds)
1644 {
1645 this.username = username;
1646 this.avatar_url = avatar_url;
1647 this.embeds = embeds;
1648 }
1649 }
1650
1651 #endregion
1652
1653 #region Public Helpers
1654 // This can be Call(ed) by other plugins to put text on a sign
1655 public void API_SignText(BasePlayer player, Signage sign, string message, int fontsize = 30, string color = "FFFFFF", string bgcolor = "000000")
1656 {
1657 //Puts($"signText called with {message}");
1658 string format = "png32";
1659
1660 ImageSize size = null;
1661 if (ImageSizePerAsset.ContainsKey(sign.ShortPrefabName))
1662 {
1663 size = ImageSizePerAsset[sign.ShortPrefabName];
1664 }
1665
1666 // Combine all the values into the url;
1667 string url = $"http://assets.imgix.net/~text?fm={format}&txtalign=middle,center&txtsize={fontsize}&txt={message}&w={size.ImageWidth}&h={size.ImageHeight}&txtclr={color}&bg={bgcolor}";
1668 imageDownloader.QueueDownload(url, player, new PaintableSignage(sign), false);
1669 }
1670
1671 public void API_SkinSign(BasePlayer player, Signage sign, string url, bool raw = false)
1672 {
1673 if (sign == null)
1674 {
1675 PrintWarning("Signage is null in API call");
1676 return;
1677 }
1678
1679 if (string.IsNullOrEmpty(url))
1680 {
1681 PrintWarning("Url is empty in API call");
1682 return;
1683 }
1684
1685 // This sign pastes in reverse, so we'll check and set a var to flip it
1686 bool hor = sign.ShortPrefabName == "sign.hanging" ? true : false;
1687
1688 // Queue the download of the specified image.
1689 imageDownloader.QueueDownload(url, player, new PaintableSignage(sign), raw, hor);
1690 }
1691
1692
1693 //TODO add image byte[] api
1694 #endregion
1695
1696 }
1697
1698 namespace SignArtistClasses
1699 {
1700 /// <summary>
1701 /// Extension class with extension methods used by the <see cref="SignArtist"/> plugin.
1702 /// </summary>
1703 public static class Extensions
1704 {
1705 /// <summary>
1706 /// Resizes an image from the <see cref="byte"/> array to a new image with a specific width and height.
1707 /// </summary>
1708 /// <param name="bytes">Source image. </param>
1709 /// <param name="width">New image canvas width. </param>
1710 /// <param name="height">New image canvas height. </param>
1711 /// <param name="targetWidth">New image width. </param>
1712 /// <param name="targetHeight">New image height. </param>
1713 /// <param name="enforceJpeg"><see cref="bool"/> value, true to save the images as JPG, false for PNG. </param>
1714 /// <param name="rotation"></param>
1715 public static byte[] ResizeImage(this byte[] bytes, int width, int height, int targetWidth, int targetHeight, bool enforceJpeg, RotateFlipType rotation = RotateFlipType.RotateNoneFlipNone)
1716 {
1717 byte[] resizedImageBytes;
1718
1719 using (MemoryStream originalBytesStream = new MemoryStream(), resizedBytesStream = new MemoryStream())
1720 {
1721 // Write the downloaded image bytes array to the memorystream and create a new Bitmap from it.
1722 originalBytesStream.Write(bytes, 0, bytes.Length);
1723 Bitmap image = new Bitmap(originalBytesStream);
1724
1725 if (rotation != RotateFlipType.RotateNoneFlipNone)
1726 {
1727 image.RotateFlip(rotation);
1728 }
1729
1730 // Check if the width and height match, if they don't we will have to resize this image.
1731 if (image.Width != targetWidth || image.Height != targetHeight)
1732 {
1733 // Create a new Bitmap with the target size.
1734 Bitmap resizedImage = new Bitmap(width, height);
1735
1736 // Draw the original image onto the new image and resize it accordingly.
1737 using (System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(resizedImage))
1738 {
1739 graphics.DrawImage(image, new Rectangle(0, 0, targetWidth, targetHeight));
1740 }
1741
1742 TimestampImage(resizedImage);
1743
1744 // Save the bitmap to a MemoryStream as either Jpeg or Png.
1745 if (enforceJpeg)
1746 {
1747 resizedImage.Save(resizedBytesStream, ImageFormat.Jpeg);
1748 }
1749 else
1750 {
1751 resizedImage.Save(resizedBytesStream, ImageFormat.Png);
1752 }
1753
1754 // Grab the bytes array from the new image's MemoryStream and dispose of the resized image Bitmap.
1755 resizedImageBytes = resizedBytesStream.ToArray();
1756 resizedImage.Dispose();
1757 }
1758 else
1759 {
1760 TimestampImage(image);
1761 // The image has the correct size so we can just return the original bytes without doing any resizing.
1762 resizedImageBytes = bytes;
1763 }
1764
1765 // Dispose of the original image Bitmap.
1766 image.Dispose();
1767 }
1768
1769 // Return the bytes array.
1770 return resizedImageBytes;
1771 }
1772
1773 /// <summary>
1774 /// Resize the image to the specified width and height.
1775 /// </summary>
1776 /// <param name="image">The image to resize.</param>
1777 /// <param name="width">The width to resize to.</param>
1778 /// <param name="height">The height to resize to.</param>
1779 /// <returns>The resized image.</returns>
1780 private static Bitmap ResizeImage(Image image, int width, int height)
1781 {
1782 Rectangle destRect = new Rectangle(0, 0, width, height);
1783 Bitmap destImage = new Bitmap(width, height);
1784
1785 destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
1786
1787 using (Graphics graphics = System.Drawing.Graphics.FromImage(destImage))
1788 {
1789 graphics.CompositingMode = CompositingMode.SourceCopy;
1790 graphics.CompositingQuality = CompositingQuality.HighQuality;
1791 graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
1792 graphics.SmoothingMode = SmoothingMode.HighQuality;
1793 graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
1794
1795 using (ImageAttributes wrapMode = new ImageAttributes())
1796 {
1797 wrapMode.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
1798 graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
1799 }
1800 }
1801
1802 return destImage;
1803 }
1804
1805 private static void TimestampImage(Bitmap image)
1806 {
1807 //Rust images are crc and if we have the same image it is deleted from the file storage
1808 //Here we changed the last few pixels of the image with colors based off the current milliseconds since wipe
1809 //This will generate a unique image every time and allow us to use the same image multiple times
1810 Color pixel = Color.FromArgb(UnityEngine.Random.Range(0, 256), UnityEngine.Random.Range(0, 256), UnityEngine.Random.Range(0, 256), UnityEngine.Random.Range(0, 256));
1811 image.SetPixel(image.Width - 1, image.Height - 1, pixel);
1812 }
1813
1814 private static int GetValueAtIndex(byte[] bytes, int index)
1815 {
1816
1817 if (index >= bytes.Length)
1818 {
1819 return 0;
1820 }
1821
1822 return Convert.ToInt32(bytes[index]);
1823
1824 }
1825
1826 /// <summary>
1827 /// Converts a string to its escaped representation for the image placeholder text value.
1828 /// </summary>
1829 /// <param name="stringToEscape">The string to escape.</param>
1830 public static string EscapeForUrl(this string stringToEscape)
1831 {
1832 // Escape initial values.
1833 stringToEscape = Uri.EscapeDataString(stringToEscape);
1834
1835 // Convert \r\n, \r and \n into linebreaks.
1836 stringToEscape = stringToEscape.Replace("%5Cr%5Cn", "%5Cn").Replace("%5Cr", "%5Cn").Replace("%5Cn", "%0A");
1837
1838 // Return the converted message
1839 return stringToEscape;
1840 }
1841 }
1842 }
1843}
1844