C#:
#region License (GPL v2)
/*
DESCRIPTION
Copyright (c) 2024 RFC1920 <desolationoutpostpve@gmail.com>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License v2.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Optionally you can also view the license at <http://www.gnu.org/licenses/>.
*/
#endregion License Information (GPL v2)
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Globalization;
using System.IO;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Photog", "RFC1920", "1.0.3")]
[Description("Paste photo from Instant Camera to a PhotoFrame")]
internal class Photog : RustPlugin
{
[PluginReference]
private readonly Plugin ImageLibrary;
private bool hasImageLibrary;
private ConfigData configData;
private const string permUse = "photog.use";
private List<ulong> isopen = new();
private const string PHOTOVIEW = "photog.imageviewer";
private const string PHOTOEDIT = "photog.imageedit";
private const string PHOTODEL = "photog.imagedel";
private Dictionary<ulong, NetworkableId> frames = new();
private Dictionary<ulong, List<UserPhotos>> photos = new();
#region Message
private string Lang(string key, string id = null, params object[] args) => string.Format(lang.GetMessage(key, this, id), args);
private void Message(IPlayer player, string key, params object[] args) => player.Message(Lang(key, player.Id, args));
private void LMessage(IPlayer player, string key, params object[] args) => player.Reply(Lang(key, player.Id, args));
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["notauthorized"] = "You don't have permission to use this command.",
["imageviewer"] = "Photog Image Viewer",
["atlimit"] = "You are at your limit for saved images. Use the /mfv command to manage them.",
["newimage"] = "New image saved as {0}",
["usure"] = "Are you sure?",
["yes"] = "Yes",
["no"] = "No",
["cancel"] = "Cancel",
["bytes"] = "bytes",
["delete"] = "Delete",
["pname"] = "Name",
["category"] = "Category",
["marked"] = "Frame has been marked",
["unmarked"] = "Frame has been unmarked"
}, this);
}
#endregion
private class UserPhotos
{
public int id;
public ulong ownerid;
public ulong photoID;
public string imageID;
public string name;
public string category;
public string date;
public string location;
public string thumbnail;
}
private void DoLog(string message)
{
if (configData.debug)
{
Interface.GetMod().LogInfo($"{Name}: {message}");
}
}
private void OnServerInitialized()
{
permission.RegisterPermission(permUse, this);
AddCovalenceCommand("mf", "cmdMarkFrame");
AddCovalenceCommand("mfv", "cmdImageManager");
LoadConfigVariables();
LoadData();
if (ImageLibrary != null) hasImageLibrary = true;
}
private void Unload()
{
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
CuiHelper.DestroyUi(player, PHOTOVIEW);
CuiHelper.DestroyUi(player, PHOTOEDIT);
CuiHelper.DestroyUi(player, PHOTODEL);
}
}
private void OnPhotoCaptured(PhotoEntity photo, Item item, BasePlayer player, byte[] numArray)
{
// Item is the camera.
DoLog($"{player.displayName} took a photo {item.name}");
if (configData.overlayPhotographerName || configData.overlayTimestamp || configData.overlayPosition)
{
string text = null;
if (configData.overlayPhotographerName) text += $"{player.displayName} ";
if (configData.overlayTimestamp) text += DateTime.Now.ToString("dd MMM yyyy");
if (configData.overlayPosition) text += $" @ {player.transform.position}";
numArray = OverlayText(numArray, text);
}
if (configData.StoreAndRetrieveUserPhotos)
{
SaveImage(player, numArray, photo.net.ID.Value, "png");
}
if (frames.ContainsKey(player.userID))
{
DoLog("Try to copy photo");
photo.SetImageData(player.userID, numArray); // This overwrites the photoItem in inventory
BaseNetworkable frame = BaseNetworkable.serverEntities.Find(frames[player.userID]);
if (frame is PhotoFrame photoFrame)
{
DoLog("Found a photo frame");
BaseNetworkable.LoadInfo info = new()
{
msg = new ProtoBuf.Entity()
{
photoFrame = new ProtoBuf.PhotoFrame()
{
ShouldPool = true,
overlayImageCrc = photo.ImageCrc,
photoEntityId = photo.net.ID,
editHistory = photo.EditingHistory
}
}
};
DoLog("Loading image to photo frame");
photoFrame.Load(info);
photoFrame.SendNetworkUpdateImmediate();
if (configData.lockOnPaint)
{
photoFrame.SetFlagLocal(BaseEntity.Flags.Locked, true);
}
}
if (!configData.leaveOpen)
{
frames.Remove(player.userID);
}
if (configData.destroyPhotoItemAfterPaste)
{
// Not working?
BaseNetworkable photoItem = BaseNetworkable.serverEntities.Find(photo.net.ID);
photoItem.DestroyShared();
player.inventory.containerMain.MarkDirty();
}
}
}
[Command("mf")]
private void cmdMarkFrame(IPlayer iplayer, string command, string[] args)
{
if (!configData.RequirePermission || (configData.RequirePermission && permission.UserHasPermission(iplayer?.Id, permUse)))
{
BasePlayer player = iplayer.Object as BasePlayer;
PhotoFrame frame = FindEntity(player) as PhotoFrame;
if (frame != null)
{
if (frames.ContainsKey(player.userID))
{
frames.Remove(player.userID);
Message(iplayer, "unmarked");
return;
}
frames.Add(player.userID, frame.NetworkID);
Message(iplayer, "marked");
}
}
}
[Command("mfv")]
private void cmdImageManager(IPlayer iplayer, string command, string[] args)
{
if (configData.RequirePermission && (!configData.RequirePermission || !permission.UserHasPermission(iplayer?.Id, permUse)))
{
Message(iplayer, "notauthorized");
return;
}
if (configData.debug)
{
string debug = string.Join(",", args); Puts($"{debug}");
}
BasePlayer player = iplayer.Object as BasePlayer;
if (!photos.ContainsKey(player.userID))
{
photos.Add(player.userID, new List<UserPhotos>());
}
if (args.Length > 0)
{
switch (args[0])
{
case "list":
string output = string.Empty;
int i = 0;
foreach (UserPhotos photo in photos[player.userID])
{
i++;
FileInfo fi = new(photo.location);
output += $"{i}: {photo.name} {fi.CreationTime}\n\t{fi.Length} " + Lang("bytes") + "\n";
}
iplayer.Reply(output);
break;
case "close":
CuiHelper.DestroyUi(player, PHOTOVIEW);
CuiHelper.DestroyUi(player, PHOTOEDIT);
CuiHelper.DestroyUi(player, PHOTODEL);
break;
case "edit":
{
ulong photoid = ulong.Parse(args[1]);
if (args.Length == 2)
{
// Open edit gui
ImageEdit(player, photoid);
}
}
break;
case "cancel":
CuiHelper.DestroyUi(player, PHOTODEL);
break;
case "delete":
{
if (args.Length > 1)
{
ulong photoid = ulong.Parse(args[1]);
if (args.Length > 2 && args[2] == "YES")
{
UserPhotos up = photos[player.userID].First(s => s.photoID == photoid);
if (up != null)
{
//string dir = Interface.GetMod().DataDirectory + $"/{Name}/{player.displayName}";
if (File.Exists(up.location))
{
File.Delete(up.location);
}
if (File.Exists(up.thumbnail))
{
File.Delete(up.thumbnail);
}
photos[player.userID].Remove(up);
SaveData();
ImageViewer(player);
}
CuiHelper.DestroyUi(player, PHOTODEL);
return;
}
ImageDelete(player, photoid);
}
}
break;
}
return;
}
ImageViewer(player);
}
private void IsOpen(ulong uid, bool set = false)
{
if (set)
{
DoLog($"Setting isopen for {uid}");
if (!isopen.Contains(uid)) isopen.Add(uid);
return;
}
DoLog($"Clearing isopen for {uid}");
isopen.Remove(uid);
}
private void ImageDelete(BasePlayer player, ulong photoid)
{
IsOpen(player.userID, true);
CuiHelper.DestroyUi(player, PHOTODEL);
CuiElementContainer container = UI.Container(PHOTODEL, UI.Color("3b3b3b", 1f), "0.25 0.4", "0.7 0.55", true, "Overlay");
UI.Label(ref container, PHOTODEL, UI.Color("#ffffff", 1f), Lang("usure"), 12, "0.1 0.92", "0.9 1");
UI.Button(ref container, PHOTODEL, UI.Color("#55d840", 1f), Lang("yes"), 12, "0.1 0.4", "0.25 0.6", $"mfv delete {photoid} YES ");
UI.Button(ref container, PHOTODEL, UI.Color("#5540d8", 1f), Lang("no"), 12, "0.27 0.4", "0.47 0.6", "mfv cancel ");
CuiHelper.AddUi(player, container);
}
private void ImageEdit(BasePlayer player, ulong photoid)
{
IsOpen(player.userID, true);
CuiHelper.DestroyUi(player, PHOTOEDIT);
}
private void ImageViewer(BasePlayer player)
{
DoLog("ImageBViewer");
IsOpen(player.userID, true);
CuiHelper.DestroyUi(player, PHOTOVIEW);
CuiElementContainer container = UI.Container(PHOTOVIEW, UI.Color("2b2b2b", 1f), "0.05 0.05", "0.95 0.95", true, "Overlay");
UI.Label(ref container, PHOTOVIEW, UI.Color("#ffffff", 1f), Lang("imageviewer"), 24, "0.2 0.92", "0.65 1");
UI.Button(ref container, PHOTOVIEW, UI.Color("#d85540", 1f), Lang("close"), 12, "0.93 0.95", "0.99 0.98", "mfv close");
int row = 0, col = 0;
//string playerdir = Interface.GetMod().DataDirectory + $"/{Name}/{player.displayName}";
foreach (UserPhotos photo in photos[player.userID])
{
row++;
if (row > 10)
{
row = 1; col += 4;
}
float[] pb = GetButtonPositionI(row, col);
//string url = $"file://{photo.thumbnail}";
//string url = photo.thumbnail;
string imageId = string.Empty;
if (hasImageLibrary)
{
imageId = (string)ImageLibrary.Call("GetImage", photo.location, 0UL);
}
//UI.Image(ref container, PHOTOVIEW, "#000000", url, $"{pb[0]} {pb[1]}", $"{pb[0] + ((pb[2] - pb[0]) / 2)} {pb[3]}");
UI.Image(ref container, PHOTOVIEW, "#000000", imageId, $"{pb[0]} {pb[1]}", $"{pb[0] + ((pb[2] - pb[0]) / 2)} {pb[3]}");
pb = GetButtonPosition(row, col + 1);
UI.Button(ref container, PHOTOVIEW, UI.Color("#55d840", 1f), Lang("edit"), 11, $"{pb[0]} {pb[1]}", $"{pb[0] + ((pb[2] - pb[0]) / 2)} {pb[3]}", $"mfv edit {photo.photoID}");
pb = GetButtonPosition(row, col + 2);
UI.Button(ref container, PHOTOVIEW, UI.Color("#55d840", 1f), Lang("delete"), 11, $"{pb[0]} {pb[1]}", $"{pb[0] + ((pb[2] - pb[0]) / 2)} {pb[3]}", $"mfv delete {photo.photoID}");
}
CuiHelper.AddUi(player, container);
}
private float[] GetButtonPosition(int rowNumber, int columnNumber, float colspan = 1f)
{
float offsetX = 0.05f + (0.126f * columnNumber);
float offsetY = (0.87f - (rowNumber * 0.164f));
Puts($"GetButtonPosition: {offsetX}, {offsetY}, {offsetX + (0.226f * colspan)}, {offsetY + 0.03f}");
return new float[] { offsetX, offsetY, offsetX + (0.226f * colspan), offsetY + 0.03f };
}
private float[] GetButtonPositionI(int rowNumber, int columnNumber)
{
float offsetX = 0.05f + (0.126f * columnNumber);
float offsetY = 0.87f - (rowNumber * 0.04f);
float offsetXr = (float)Math.Round((offsetX + 0.08f) * 1000f) / 1000f;
float offsetYr = (float)Math.Round((offsetY + 0.08f) * 1000f) / 1000f;
Puts($"GetButtonPositionI: {offsetX}, {offsetY}, {offsetXr}, {offsetYr}");
return new float[] { offsetX, offsetY, offsetXr, offsetYr };
}
private int CountFiles(BasePlayer player)
{
try
{
string dir = Interface.GetMod().DataDirectory + $"/{Name}/{player.displayName}";
int count = Directory.GetFiles(dir, "*.*").Count(s => s.EndsWith(".png") || s.EndsWith(".jpg"));
DoLog($"{player.displayName} has {count} files");
return count;
}
catch { return 0; }
}
private void SaveImage(BasePlayer player, byte[] numArray, ulong photoID, string format = "png")
{
if (CountFiles(player) >= configData.StoreAndRetrieveLimit)
{
Message(player.IPlayer, "atlimit");
return;
}
DoLog($"SaveImage called for {player?.displayName}");
if (!photos.ContainsKey(player.userID))
{
photos.Add(player.userID, new List<UserPhotos>());
}
string dir = Interface.GetMod().DataDirectory + $"/{Name}/{player.displayName}";
string filename = DateTime.Now.ToString("ddMMyy_hhmmss") + $".{format}";
Bitmap bmp;
using (MemoryStream m = new(numArray))
{
bmp = new Bitmap(m);
}
// Create player data folder
Core.Configuration.DynamicConfigFile x = Interface.Oxide.DataFileSystem.GetFile($"{dir}/info");
x.WriteObject(player.displayName);
DoLog($"Saving image as {dir}/{filename}");
switch (format)
{
case "jpg":
case "jpeg":
DoLog("Saving as JPEG");
bmp.Save($"{dir}/{filename}", System.Drawing.Imaging.ImageFormat.Jpeg);
break;
default:
DoLog("Saving as PNG");
bmp.Save($"{dir}/{filename}", System.Drawing.Imaging.ImageFormat.Png);
break;
}
Message(player.IPlayer, "newimage", filename);
DoLog("Saving thumbnail image");
Image image = Image.FromFile($"{dir}/{filename}");
Image thumb = image.GetThumbnailImage(120, 80, () => false, IntPtr.Zero);
string thumbpath = Path.ChangeExtension($"{dir}/{filename}", "thumb.png");
thumb.Save(thumbpath);
DoLog("Adding image record");
string imageId = string.Empty;
if (hasImageLibrary)
{
ImageLibrary.Call("AddImage", $"{dir}/{filename}", filename, 0UL);
string imgid = (string)ImageLibrary.Call("GetImage", filename);
}
photos[player.userID].Add(new UserPhotos()
{
id = UnityEngine.Random.Range(1, 99999999),
date = DateTime.Now.ToString(),
location = $"{dir}/{filename}",
thumbnail = thumbpath,
name = filename,
ownerid = player.userID,
photoID = photoID,
imageID = imageId
});
SaveData();
}
private byte[] OverlayText(byte[] numArray, string text)
{
// https://stackoverflow.com/questions/40041373/add-text-to-image-without-saving
DoLog("OverlayText");
Bitmap bmp;
using (MemoryStream m = new(numArray))
{
bmp = new Bitmap(m);
}
DoLog($"IMG w{bmp.Width}/h{bmp.Height}");
// Always 854x480
int sLeft = bmp.Width / 6;// * 4;
int sHeight = bmp.Height / 5 * 4;
DoLog($"Overlay text '{text}' at {sHeight}/{sLeft} h 50 w 650");
RectangleF rectf = new(sLeft, sHeight, 650, 50);
int fontsize = configData.overlayFontSize > 10 ? configData.overlayFontSize : 10;
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
{
Brush brush = configData.blackTextInsteadOfWhite ? Brushes.Black : Brushes.White;
using (System.Drawing.Font arialFont = new("Arial", fontsize))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
g.DrawString(text, new System.Drawing.Font("courier sans", fontsize, System.Drawing.FontStyle.Bold), brush, rectf);
}
}
MemoryStream ms = new();
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
byte[] arr = new byte[ms.Length];
ms.Position = 0;
ms.Read(arr, 0, (int)ms.Length);
ms.Close();
return arr;
}
private static BaseEntity FindEntity(BasePlayer player)
{
RaycastHit hit;
if (!Physics.Raycast(player.eyes.HeadRay(), out hit)) return null;
return hit.GetEntity();
}
public class ConfigData
{
public bool lockOnPaint;
public bool leaveOpen;
public bool overlayPhotographerName;
public bool overlayTimestamp;
public bool overlayPosition;
public int overlayFontSize;
public bool blackTextInsteadOfWhite;
public bool destroyPhotoItemAfterPaste;
public bool debug;
public bool RequirePermission;
public bool StoreAndRetrieveUserPhotos;
public int StoreAndRetrieveLimit;
public VersionNumber Version;
}
private void LoadConfigVariables()
{
configData = Config.ReadObject<ConfigData>();
if (configData.Version < new VersionNumber(1, 0, 3))
{
configData.StoreAndRetrieveLimit = 20;
}
configData.Version = Version;
SaveConfig(configData);
}
private void SaveConfig(ConfigData config)
{
Config.WriteObject(config, true);
}
protected override void LoadDefaultConfig()
{
DoLog("Creating new config file.");
ConfigData config = new()
{
lockOnPaint = false,
leaveOpen = false,
overlayPhotographerName = true,
overlayFontSize = 12,
debug = false,
RequirePermission = false,
StoreAndRetrieveLimit = 20
};
}
private void LoadData()
{
photos = Interface.GetMod().DataFileSystem.ReadObject<Dictionary<ulong, List<UserPhotos>>>(Name + "/userphotos");
bool badid = false;
foreach (KeyValuePair<ulong, List<UserPhotos>> userdata in photos)
{
foreach (UserPhotos photo in userdata.Value)
{
if (photo.id == 0)
{
badid = true;
photo.id = UnityEngine.Random.Range(1, 99999999);
}
}
}
if (badid) { SaveData(); }
}
private void SaveData()
{
Interface.GetMod().DataFileSystem.WriteObject(Name + "/userphotos", photos);
}
public static class UI
{
public static CuiElementContainer Container(string panel, string color, string min, string max, bool useCursor = false, string parent = "Overlay")
{
return new CuiElementContainer()
{
{
new CuiPanel
{
Image = { Color = color },
RectTransform = {AnchorMin = min, AnchorMax = max},
CursorEnabled = useCursor
},
new CuiElement().Parent = parent,
panel
}
};
}
public static void Panel(ref CuiElementContainer container, string panel, string color, string min, string max, bool cursor = false)
{
container.Add(new CuiPanel
{
Image = { Color = color },
RectTransform = { AnchorMin = min, AnchorMax = max },
CursorEnabled = cursor
},
panel);
}
public static void Label(ref CuiElementContainer container, string panel, string color, string text, int size, string min, string max, TextAnchor align = TextAnchor.MiddleCenter)
{
container.Add(new CuiLabel
{
Text = { Color = color, FontSize = size, Align = align, Text = text },
RectTransform = { AnchorMin = min, AnchorMax = max }
},
panel);
}
public static void Button(ref CuiElementContainer container, string panel, string color, string text, int size, string min, string max, string command, TextAnchor align = TextAnchor.MiddleCenter)
{
container.Add(new CuiButton
{
Button = { Color = color, Command = command, FadeIn = 0f },
RectTransform = { AnchorMin = min, AnchorMax = max },
Text = { Text = text, FontSize = size, Align = align }
},
panel);
}
public static void Input(ref CuiElementContainer container, string panel, string color, string text, int size, string min, string max, string command, TextAnchor align = TextAnchor.MiddleCenter)
{
container.Add(new CuiElement
{
Name = CuiHelper.GetGuid(),
Parent = panel,
Components =
{
new CuiInputFieldComponent
{
Align = align,
CharsLimit = 30,
Color = color,
Command = command + text,
FontSize = size,
IsPassword = false,
Text = text,
NeedsKeyboard = true
},
new CuiRectTransformComponent { AnchorMin = min, AnchorMax = max },
new CuiNeedsCursorComponent()
}
});
}
public static void Icon(ref CuiElementContainer container, string panel, string color, string imageurl, string min, string max)
{
container.Add(new CuiElement
{
Name = CuiHelper.GetGuid(),
Parent = panel,
Components =
{
new CuiRawImageComponent
{
Url = imageurl,
//Png = imageurl,
//Sprite = imageurl,
Sprite = "assets/content/textures/generic/fulltransparent.tga",
Color = color
},
new CuiRectTransformComponent
{
AnchorMin = min,
AnchorMax = max
}
}
});
}
public static void Image(ref CuiElementContainer container, string panel, string color, string imageurl, string min, string max)
{
container.Add(new CuiElement
{
Name = CuiHelper.GetGuid(),
Parent = panel,
Components =
{
new CuiRawImageComponent
{
Png = imageurl,
Sprite = "assets/content/textures/generic/fulltransparent.tga",
Color = color
},
new CuiRectTransformComponent
{
AnchorMin = min,
AnchorMax = max
}
}
});
}
public static string Color(string hexColor, float alpha)
{
if (hexColor.StartsWith("#"))
{
hexColor = hexColor.Substring(1);
}
int red = int.Parse(hexColor.Substring(0, 2), NumberStyles.AllowHexSpecifier);
int green = int.Parse(hexColor.Substring(2, 2), NumberStyles.AllowHexSpecifier);
int blue = int.Parse(hexColor.Substring(4, 2), NumberStyles.AllowHexSpecifier);
return $"{(double)red / 255} {(double)green / 255} {(double)blue / 255} {alpha}";
}
}
}
}