C#:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("MyPlayerBuilds", "Danevi/RustFlash", "2.2.0")]
[Description("Allows players to save and load their own builds")]
class MyPlayerBuilds : RustPlugin
{
[PluginReference]
private Plugin CopyPaste;
#region Configuration
private Configuration config;
private class Configuration
{
[JsonProperty("Maximum bases per player")]
public int MaxBasesPerPlayer = 10;
[JsonProperty("Save cooldown in seconds")]
public float SaveCooldown = 60f;
[JsonProperty("Enable automatic backups")]
public bool EnableBackups = true;
[JsonProperty("Debug mode")]
public bool DebugMode = false;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<Configuration>();
if (config == null)
{
LoadDefaultConfig();
}
}
catch
{
PrintWarning("Creating new configuration file");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
config = new Configuration();
}
protected override void SaveConfig()
{
Config.WriteObject(config);
}
#endregion
#region Stored Data
private StoredData storedData;
private Dictionary<ulong, DateTime> cooldowns = new Dictionary<ulong, DateTime>();
private Dictionary<ulong, string> pendingSaves = new Dictionary<ulong, string>();
private class StoredData
{
public Dictionary<ulong, PlayerData> players = new Dictionary<ulong, PlayerData>();
}
private class PlayerData
{
public List<string> savedBases = new List<string>();
public DateTime lastSave = DateTime.MinValue;
}
private void LoadData()
{
try
{
storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>("MyPlayerBuilds");
}
catch
{
storedData = new StoredData();
}
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject("MyPlayerBuilds", storedData);
}
#endregion
#region Initialization
void Init()
{
permission.RegisterPermission("myplayerbuilds.use", this);
permission.RegisterPermission("myplayerbuilds.admin", this);
LoadData();
cmd.AddChatCommand("save", this, SaveCommand);
cmd.AddChatCommand("load", this, LoadCommand);
cmd.AddChatCommand("listbases", this, ListCommand);
cmd.AddChatCommand("deletebase", this, DeleteCommand);
}
void OnServerInitialized()
{
if (CopyPaste == null)
{
PrintError("CopyPaste plugin is required but not loaded!");
Interface.Oxide.UnloadPlugin(Name);
}
}
void Unload()
{
SaveData();
}
#endregion
#region Hook Methods
void OnCopyFinished(object rawData, string filename, object player, object sourcePos)
{
DebugLog($"OnCopyFinished called for file: {filename}");
if (player is Oxide.Core.Libraries.Covalence.IPlayer iPlayer &&
iPlayer.Object is BasePlayer basePlayer)
{
if (pendingSaves.ContainsKey(basePlayer.userID) &&
pendingSaves[basePlayer.userID] == filename)
{
DebugLog($"Processing pending save for {basePlayer.displayName}: {filename}");
timer.Once(1f, () => ProcessSaveFromHook(basePlayer, filename));
}
}
}
#endregion
#region Helper Methods
private void DebugLog(string message)
{
if (config.DebugMode)
{
Puts($"[DEBUG] {message}");
}
}
private void ProcessSaveFromHook(BasePlayer player, string baseName)
{
try
{
string sourceFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}copypaste{Path.DirectorySeparatorChar}{baseName}.json";
string playerDir = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{player.userID}";
string destFile = $"{playerDir}{Path.DirectorySeparatorChar}{baseName}.json";
DebugLog($"ProcessSaveFromHook: Source={sourceFile}, Dest={destFile}");
DebugLog($"Source exists: {System.IO.File.Exists(sourceFile)}");
if (!System.IO.File.Exists(sourceFile))
{
SendReply(player, lang.GetMessage("SaveError", this, player.UserIDString), "CopyPaste file not found");
return;
}
if (!System.IO.Directory.Exists(playerDir))
System.IO.Directory.CreateDirectory(playerDir);
if (System.IO.File.Exists(destFile) && config.EnableBackups)
{
string backupFile = $"{playerDir}{Path.DirectorySeparatorChar}{baseName}_backup_{DateTime.Now:yyyyMMddHHmmss}.json";
System.IO.File.Copy(destFile, backupFile);
DebugLog($"Created backup: {backupFile}");
}
System.IO.File.Copy(sourceFile, destFile, true);
DebugLog($"Copied file to: {destFile}");
if (!storedData.players.ContainsKey(player.userID))
storedData.players[player.userID] = new PlayerData();
var playerData = storedData.players[player.userID];
if (!playerData.savedBases.Contains(baseName))
playerData.savedBases.Add(baseName);
playerData.lastSave = DateTime.Now;
SaveData();
pendingSaves.Remove(player.userID);
SendReply(player, lang.GetMessage("SaveSuccess", this, player.UserIDString), baseName);
DebugLog($"Successfully saved base: {baseName}");
Interface.CallHook("OnPlayerBuildSaved", player, baseName);
}
catch (Exception ex)
{
DebugLog($"ProcessSaveFromHook error: {ex}");
SendReply(player, lang.GetMessage("SaveError", this, player.UserIDString), ex.Message);
pendingSaves.Remove(player.userID);
}
}
#endregion
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You don't have permission to use this command.",
["ProvideName"] = "Usage: /{0} <name>",
["InvalidName"] = "Invalid name. Use only letters, numbers, - and _",
["OnCooldown"] = "Please wait {0} seconds before saving again.",
["MaxBases"] = "You have reached the maximum of {0} bases.",
["SaveSuccess"] = "Base '{0}' saved successfully!",
["SaveError"] = "Error saving base: {0}",
["LoadSuccess"] = "Base '{0}' loaded successfully!",
["LoadError"] = "Error loading base: {0}",
["BaseNotFound"] = "Base '{0}' not found.",
["NoBases"] = "You have no saved bases.",
["BasesList"] = "Your saved bases:\n{0}",
["DeleteSuccess"] = "Base '{0}' deleted successfully.",
["NoCopyPaste"] = "CopyPaste plugin is not loaded!",
["LookAtGround"] = "You need to look at the ground where you want to load your base!",
["LookAtBuilding"] = "You need to look at a building block (foundation, wall, etc.) to save!",
["SaveInProgress"] = "Saving base '{0}', please wait...",
["LoadInProgress"] = "Loading base '{0}', please wait..."
}, this);
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "Du hast keine Berechtigung für diesen Befehl.",
["ProvideName"] = "Verwendung: /{0} <Name>",
["InvalidName"] = "Ungültiger Name. Nur Buchstaben, Zahlen, - und _ erlaubt.",
["OnCooldown"] = "Bitte warte {0} Sekunden vor dem nächsten Speichern.",
["MaxBases"] = "Du hast das Maximum von {0} Basen erreicht.",
["SaveSuccess"] = "Basis '{0}' erfolgreich gespeichert!",
["SaveError"] = "Fehler beim Speichern: {0}",
["LoadSuccess"] = "Basis '{0}' erfolgreich geladen!",
["LoadError"] = "Fehler beim Laden: {0}",
["BaseNotFound"] = "Basis '{0}' nicht gefunden.",
["NoBases"] = "Du hast keine gespeicherten Basen.",
["BasesList"] = "Deine gespeicherten Basen:\n{0}",
["DeleteSuccess"] = "Basis '{0}' erfolgreich gelöscht.",
["NoCopyPaste"] = "CopyPaste Plugin ist nicht geladen!",
["LookAtGround"] = "Du musst auf den Boden schauen, wo du deine Basis laden möchtest!",
["LookAtBuilding"] = "Du musst auf einen Baublock (Foundation, Wand, etc.) schauen zum Speichern!",
["SaveInProgress"] = "Speichere Basis '{0}', bitte warten...",
["LoadInProgress"] = "Lade Basis '{0}', bitte warten..."
}, this, "de");
}
#endregion
#region Commands
void SaveCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "myplayerbuilds.use"))
{
SendReply(player, lang.GetMessage("NoPermission", this, player.UserIDString));
return;
}
if (CopyPaste == null || !CopyPaste.IsLoaded)
{
SendReply(player, lang.GetMessage("NoCopyPaste", this, player.UserIDString));
return;
}
if (args.Length == 0)
{
SendReply(player, lang.GetMessage("ProvideName", this, player.UserIDString), "save");
return;
}
string baseName = args[0];
if (!System.Text.RegularExpressions.Regex.IsMatch(baseName, @"^[a-zA-Z0-9_-]+$"))
{
SendReply(player, lang.GetMessage("InvalidName", this, player.UserIDString));
return;
}
if (cooldowns.ContainsKey(player.userID))
{
var timeSince = DateTime.Now - cooldowns[player.userID];
if (timeSince.TotalSeconds < config.SaveCooldown)
{
var remaining = config.SaveCooldown - timeSince.TotalSeconds;
SendReply(player, lang.GetMessage("OnCooldown", this, player.UserIDString),
Math.Ceiling(remaining).ToString());
return;
}
}
if (!storedData.players.ContainsKey(player.userID))
storedData.players[player.userID] = new PlayerData();
var playerData = storedData.players[player.userID];
if (playerData.savedBases.Count >= config.MaxBasesPerPlayer)
{
SendReply(player, lang.GetMessage("MaxBases", this, player.UserIDString),
config.MaxBasesPerPlayer.ToString());
return;
}
RaycastHit hit;
if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 100f))
{
SendReply(player, lang.GetMessage("LookAtBuilding", this, player.UserIDString));
return;
}
var entity = hit.GetEntity();
if (entity == null || entity.PrefabName == null ||
(!entity.PrefabName.Contains("foundation") && !entity.PrefabName.Contains("wall") &&
!entity.PrefabName.Contains("floor") && !entity.PrefabName.Contains("roof")))
{
SendReply(player, lang.GetMessage("LookAtBuilding", this, player.UserIDString));
return;
}
DebugLog($"Player {player.displayName} saving base '{baseName}', looking at {entity.PrefabName}");
pendingSaves[player.userID] = baseName;
SendReply(player, lang.GetMessage("SaveInProgress", this, player.UserIDString), baseName);
var cmdArgs = new List<string> { baseName };
cmdArgs.AddRange(args.Skip(1));
DebugLog($"Executing copy command with args: {string.Join(" ", cmdArgs)}");
rust.RunClientCommand(player, "copy", cmdArgs.ToArray());
cooldowns[player.userID] = DateTime.Now;
timer.Once(10f, () =>
{
if (pendingSaves.ContainsKey(player.userID) && pendingSaves[player.userID] == baseName)
{
pendingSaves.Remove(player.userID);
SendReply(player, lang.GetMessage("SaveError", this, player.UserIDString), "Save timeout - try looking directly at a foundation block");
}
});
}
void LoadCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "myplayerbuilds.use"))
{
SendReply(player, lang.GetMessage("NoPermission", this, player.UserIDString));
return;
}
if (CopyPaste == null || !CopyPaste.IsLoaded)
{
SendReply(player, lang.GetMessage("NoCopyPaste", this, player.UserIDString));
return;
}
if (args.Length == 0)
{
SendReply(player, lang.GetMessage("ProvideName", this, player.UserIDString), "load");
return;
}
string baseName = args[0];
string playerFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{player.userID}{Path.DirectorySeparatorChar}{baseName}.json";
if (!System.IO.File.Exists(playerFile))
{
SendReply(player, lang.GetMessage("BaseNotFound", this, player.UserIDString), baseName);
return;
}
RaycastHit hit;
if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 1000f))
{
SendReply(player, lang.GetMessage("LookAtGround", this, player.UserIDString));
return;
}
DebugLog($"Loading base '{baseName}' for player {player.displayName}");
try
{
string tempFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}copypaste{Path.DirectorySeparatorChar}{baseName}.json";
DebugLog($"Copying {playerFile} to {tempFile}");
System.IO.File.Copy(playerFile, tempFile, true);
if (!System.IO.File.Exists(tempFile))
{
SendReply(player, lang.GetMessage("LoadError", this, player.UserIDString), "Failed to prepare file for loading");
return;
}
SendReply(player, lang.GetMessage("LoadInProgress", this, player.UserIDString), baseName);
var pasteArgs = new List<string> { baseName };
if (args.Length > 1)
pasteArgs.AddRange(args.Skip(1));
else
pasteArgs.AddRange(new[] { "autoheight", "true" });
DebugLog($"Executing paste command with args: {string.Join(" ", pasteArgs)}");
rust.RunClientCommand(player, "paste", pasteArgs.ToArray());
timer.Once(2f, () =>
{
SendReply(player, lang.GetMessage("LoadSuccess", this, player.UserIDString), baseName);
Interface.CallHook("OnPlayerBuildLoaded", player, baseName);
});
timer.Once(5f, () =>
{
try
{
if (System.IO.File.Exists(tempFile))
{
System.IO.File.Delete(tempFile);
DebugLog($"Cleaned up temp file: {tempFile}");
}
}
catch (Exception ex)
{
DebugLog($"Error cleaning up temp file: {ex.Message}");
}
});
}
catch (Exception ex)
{
DebugLog($"LoadCommand error: {ex}");
SendReply(player, lang.GetMessage("LoadError", this, player.UserIDString), ex.Message);
}
}
void ListCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "myplayerbuilds.use"))
{
SendReply(player, lang.GetMessage("NoPermission", this, player.UserIDString));
return;
}
if (!storedData.players.ContainsKey(player.userID) ||
storedData.players[player.userID].savedBases.Count == 0)
{
SendReply(player, lang.GetMessage("NoBases", this, player.UserIDString));
return;
}
var bases = string.Join("\n", storedData.players[player.userID].savedBases.Select(b => $"• {b}"));
SendReply(player, lang.GetMessage("BasesList", this, player.UserIDString), bases);
}
void DeleteCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "myplayerbuilds.use"))
{
SendReply(player, lang.GetMessage("NoPermission", this, player.UserIDString));
return;
}
if (args.Length == 0)
{
SendReply(player, lang.GetMessage("ProvideName", this, player.UserIDString), "deletebase");
return;
}
string baseName = args[0];
string playerFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{player.userID}{Path.DirectorySeparatorChar}{baseName}.json";
if (!System.IO.File.Exists(playerFile))
{
SendReply(player, lang.GetMessage("BaseNotFound", this, player.UserIDString), baseName);
return;
}
try
{
System.IO.File.Delete(playerFile);
if (storedData.players.ContainsKey(player.userID))
{
storedData.players[player.userID].savedBases.Remove(baseName);
SaveData();
}
SendReply(player, lang.GetMessage("DeleteSuccess", this, player.UserIDString), baseName);
Interface.CallHook("OnPlayerBuildDeleted", player, baseName);
}
catch (Exception ex)
{
SendReply(player, lang.GetMessage("SaveError", this, player.UserIDString), ex.Message);
}
}
#endregion
#region Console Commands (for debugging)
[ConsoleCommand("myplayerbuilds.debug")]
void DebugCommand(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin)
return;
if (arg.Args == null || arg.Args.Length == 0)
{
arg.ReplyWith("Usage: myplayerbuilds.debug <on/off>");
return;
}
switch (arg.Args[0].ToLower())
{
case "on":
config.DebugMode = true;
SaveConfig();
arg.ReplyWith("Debug mode enabled");
break;
case "off":
config.DebugMode = false;
SaveConfig();
arg.ReplyWith("Debug mode disabled");
break;
default:
arg.ReplyWith("Usage: myplayerbuilds.debug <on/off>");
break;
}
}
[ConsoleCommand("myplayerbuilds.testload")]
void TestLoadCommand(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin)
return;
if (arg.Args == null || arg.Args.Length < 2)
{
arg.ReplyWith("Usage: myplayerbuilds.testload <playerId> <baseName>");
return;
}
if (!ulong.TryParse(arg.Args[0], out ulong playerId))
{
arg.ReplyWith("Invalid player ID");
return;
}
string baseName = arg.Args[1];
string playerFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{playerId}{Path.DirectorySeparatorChar}{baseName}.json";
arg.ReplyWith($"Checking file: {playerFile}");
arg.ReplyWith($"File exists: {System.IO.File.Exists(playerFile)}");
if (System.IO.File.Exists(playerFile))
{
try
{
var content = System.IO.File.ReadAllText(playerFile);
arg.ReplyWith($"File size: {content.Length} characters");
arg.ReplyWith($"Content preview: {content.Substring(0, Math.Min(200, content.Length))}...");
}
catch (Exception ex)
{
arg.ReplyWith($"Error reading file: {ex.Message}");
}
}
string copyPasteFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}copypaste{Path.DirectorySeparatorChar}{baseName}.json";
arg.ReplyWith($"CopyPaste file exists: {System.IO.File.Exists(copyPasteFile)}");
}
[ConsoleCommand("myplayerbuilds.listsaves")]
void ListSavesCommand(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin)
return;
if (arg.Args == null || arg.Args.Length < 1)
{
arg.ReplyWith("Usage: myplayerbuilds.listsaves <playerId>");
return;
}
if (!ulong.TryParse(arg.Args[0], out ulong playerId))
{
arg.ReplyWith("Invalid player ID");
return;
}
string playerDir = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{playerId}";
if (!System.IO.Directory.Exists(playerDir))
{
arg.ReplyWith("Player has no saved bases directory");
return;
}
var files = System.IO.Directory.GetFiles(playerDir, "*.json")
.Select(f => Path.GetFileNameWithoutExtension(f))
.ToArray();
arg.ReplyWith($"Found {files.Length} base files: {string.Join(", ", files)}");
}
#endregion
#region API
private bool SavePlayerBuild(ulong userId, string baseName, string buildData)
{
try
{
string playerDir = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{userId}";
if (!System.IO.Directory.Exists(playerDir))
System.IO.Directory.CreateDirectory(playerDir);
string destFile = $"{playerDir}{Path.DirectorySeparatorChar}{baseName}.json";
System.IO.File.WriteAllText(destFile, buildData);
if (!storedData.players.ContainsKey(userId))
storedData.players[userId] = new PlayerData();
if (!storedData.players[userId].savedBases.Contains(baseName))
storedData.players[userId].savedBases.Add(baseName);
SaveData();
return true;
}
catch
{
return false;
}
}
private string GetPlayerBuild(ulong userId, string baseName)
{
try
{
string playerFile = $"{Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}MyPlayerBuilds{Path.DirectorySeparatorChar}{userId}{Path.DirectorySeparatorChar}{baseName}.json";
if (!System.IO.File.Exists(playerFile))
return null;
return System.IO.File.ReadAllText(playerFile);
}
catch
{
return null;
}
}
private List<string> GetPlayerBuilds(ulong userId)
{
if (!storedData.players.ContainsKey(userId))
return new List<string>();
return storedData.players[userId].savedBases;
}
#endregion
}
}