C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Blueprint Share", "c_creep, modified by SeesAll", "1.4.8")]
[Description("Allows players to share researched blueprints with their friends, clan or team")]
class BlueprintShare : RustPlugin
{
#region Fields
[PluginReference]
private Plugin Clans, Friends;
private StoredData storedData;
private Dictionary<string, Action<BasePlayer, string[]>> chatCommandHandlers;
private Dictionary<ulong, List<ulong>> shareTargetCache = new Dictionary<ulong, List<ulong>>();
private Dictionary<ulong, Timer> shareTargetCacheTimers = new Dictionary<ulong, Timer>();
private readonly HashSet<string> normalTechTreeUnlocks = new HashSet<string>();
private const float shareTargetCacheDuration = 10f; // Seconds
private enum ShareType
{
Teams,
Friends,
Clans
}
private enum DebugLevel
{
Info,
Warning,
Error
}
private const string usePermission = "blueprintshare.use";
private const string togglePermission = "blueprintshare.toggle";
private const string sharePermission = "blueprintshare.share";
private const string showPermission = "blueprintshare.show";
private const string bypassPermission = "blueprintshare.bypass";
private bool isTeamsEnabled;
private bool isClansEnabled;
private bool isFriendsEnabled;
private bool dataDirty = false;
private Timer pendingDataSaveTimer;
private const float dataSaveDebounceDelay = 30f; // Seconds
#endregion
#region Oxide Hooks
private void Init()
{
LoadData();
permission.RegisterPermission(usePermission, this);
permission.RegisterPermission(togglePermission, this);
permission.RegisterPermission(sharePermission, this);
permission.RegisterPermission(showPermission, this);
permission.RegisterPermission(bypassPermission, this);
isTeamsEnabled = config.TeamsEnabled;
isClansEnabled = config.ClansEnabled;
isFriendsEnabled = config.FriendsEnabled;
chatCommandHandlers = new Dictionary<string, Action<BasePlayer, string[]>>
{
["help"] = HelpCommand,
["toggle"] = ToggleCommand,
["share"] = ShareCommand,
["show"] = ShowCommand
};
if (!isTeamsEnabled)
{
Unsubscribe(nameof(OnTeamAcceptInvite));
Unsubscribe(nameof(OnTeamKick));
Unsubscribe(nameof(OnTeamLeave));
}
if (!isClansEnabled)
{
Unsubscribe(nameof(OnClanMemberJoined));
Unsubscribe(nameof(OnClanMemberGone));
Unsubscribe(nameof(OnClanDisbanded));
}
if (!isFriendsEnabled)
{
Unsubscribe(nameof(OnFriendAdded));
Unsubscribe(nameof(OnFriendRemoved));
}
}
private void OnNewSave(string filename)
{
if (!config.ClearDataOnWipe) return;
CreateData();
}
private void OnServerShutdown()
{
FlushDataSave();
CancelScheduledDataSave();
}
private void OnServerSave()
{
FlushDataSave();
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (dataDirty)
{
ScheduleDataSave();
}
}
private void OnPlayerConnected(BasePlayer player)
{
GetPlayerData(player.UserIDString);
if (config.ReceiveMessagesEnabled)
{
timer.Once(1f, () =>
{
if (player != null && player.IsConnected)
{
DisplayPendingNotifications(player);
}
});
}
}
private void OnItemAction(Item item, string action, BasePlayer player)
{
if (!config.ItemSharingEnabled) return;
if (player == null || item == null) return;
if (action != "study") return;
DebugLog($"{player.displayName} Started Blueprint Item Unlock For {item.blueprintTargetDef.displayName.translated}");
if (TryShareBlueprint(item.blueprintTargetDef, player))
{
item.Remove();
}
}
private void OnTechTreeNodeUnlock(Workbench workbench, TechTreeData.NodeInstance node, BasePlayer player)
{
if (!config.TechTreeSharingEnabled || workbench == null || node?.itemDef == null || player == null) return;
if (!workbench.HasTechTreeBypassUpgrade()) return;
var key = GetTechTreeUnlockKey(player.userID, node.itemDef.itemid);
timer.Once(0.1f, () =>
{
if (player == null || !player.IsConnected || node?.itemDef == null) return;
if (!player.blueprints.HasUnlocked(node.itemDef)) return;
if (normalTechTreeUnlocks.Contains(key)) return;
HandleTechTreeUnlock(player, new[] { node.itemDef });
});
}
private void OnTechTreeNodeUnlocked(Workbench workbench, TechTreeData.NodeInstance node, BasePlayer player, PooledList<ItemDefinition> items)
{
if (!config.TechTreeSharingEnabled || player == null || node?.itemDef == null || items == null) return;
var key = GetTechTreeUnlockKey(player.userID, node.itemDef.itemid);
normalTechTreeUnlocks.Add(key);
HandleTechTreeUnlock(player, items);
timer.Once(1f, () => normalTechTreeUnlocks.Remove(key));
}
private void HandleTechTreeUnlock(BasePlayer player, IEnumerable<ItemDefinition> items)
{
if (player == null || items == null) return;
foreach (var item in items)
{
if (item == null) continue;
DebugLog($"{player.displayName} Started TechTree Unlock For {item.displayName.translated}");
TryShareBlueprint(item, player);
}
}
private static string GetTechTreeUnlockKey(ulong playerId, int itemId) => $"{playerId}:{itemId}";
private void OnTeamAcceptInvite(RelationshipManager.PlayerTeam team, BasePlayer joiningPlayer)
{
if (!config.ShareToExistingMembers && !config.ShareToNewMembers) return;
if (team == null || joiningPlayer == null) return;
DebugLog($"{joiningPlayer.displayName} joined a team");
timer.Once(1f, () =>
{
var teamMemberIds = team.members;
if (teamMemberIds == null || teamMemberIds.Count == 0) return;
var teamMembers = FindPlayersByIds(teamMemberIds, joiningPlayer.userID);
if (teamMembers.Count == 0) return;
// Share FROM joining player TO each team member
if (config.ShareToExistingMembers)
{
foreach (var member in teamMembers)
{
if (member == null || member == joiningPlayer) continue;
ShareWithPlayer(joiningPlayer, member); // member receives from joiner
}
}
// Share FROM each team member TO joining player
if (config.ShareToNewMembers)
{
foreach (var member in teamMembers)
{
if (member == null || member == joiningPlayer) continue;
ShareWithPlayer(member, joiningPlayer); // joiner receives from member
}
}
});
}
private void OnTeamLeave(RelationshipManager.PlayerTeam team, BasePlayer player)
{
DebugLog($"{player.displayName} left their team");
ClearShareTargetCache(player.userID);
PlayerLeftTeam(player.userID);
}
private void OnTeamKick(RelationshipManager.PlayerTeam team, BasePlayer player, ulong targetId)
{
DebugLog($"{targetId} was kicked from their team by {player.displayName}");
ClearShareTargetCaches(player.userID, targetId);
PlayerLeftTeam(targetId);
}
#endregion
#region External Plugins
#region Friends
private bool HasFriends(ulong playerId)
{
if (Friends == null || !Friends.IsLoaded) return false;
var friendsList = Friends.Call<ulong[]>("GetFriends", playerId);
return friendsList != null && friendsList.Length != 0;
}
private List<ulong> GetFriends(ulong playerId)
{
if (Friends == null || !Friends.IsLoaded) return new List<ulong>();
var friends = Friends.Call<ulong[]>("GetFriends", playerId);
return friends.ToList();
}
private bool IsFriend(ulong playerId, ulong targetId)
{
return Friends != null && Friends.IsLoaded && Friends.Call<bool>("HasFriend", playerId, targetId);
}
private bool AreMutualFriends(ulong playerId, ulong targetId)
{
return Friends != null && Friends.IsLoaded && Friends.Call<bool>("AreFriends", playerId, targetId);
}
#endregion
#region Clan
private bool InClan(ulong playerId)
{
if (Clans == null || !Clans.IsLoaded) return false;
var clanName = Clans.Call<string>("GetClanOf", playerId);
return !string.IsNullOrEmpty(clanName);
}
private List<ulong> GetClanMembers(ulong playerId)
{
var memberIds = new HashSet<ulong>();
if (Clans == null || !Clans.IsLoaded) return memberIds.ToList();
var currentApiMembers = Clans.Call("GetClanMembers", playerId);
if (currentApiMembers is IEnumerable<ulong> ulongMembers)
{
memberIds.UnionWith(ulongMembers);
}
else if (currentApiMembers is IEnumerable<string> stringMembers)
{
foreach (var memberId in stringMembers)
{
if (ulong.TryParse(memberId, out var parsedMemberId))
{
memberIds.Add(parsedMemberId);
}
}
}
if (memberIds.Count > 0) return memberIds.ToList();
// Compatibility fallback for older Clans implementations.
var clanTag = Clans.Call<string>("GetClanOf", playerId);
if (string.IsNullOrEmpty(clanTag)) return memberIds.ToList();
var clanInfo = Clans.Call<JObject>("GetClan", clanTag);
if (clanInfo?["members"] == null) return memberIds.ToList();
foreach (var memberToken in clanInfo["members"])
{
if (ulong.TryParse(memberToken.ToString(), out var parsedMemberId))
{
memberIds.Add(parsedMemberId);
}
}
return memberIds.ToList();
}
private bool SameClan(ulong playerId, ulong targetId)
{
if (Clans == null || !Clans.IsLoaded) return false;
var currentApiResult = Clans.Call("IsClanMember", playerId, targetId);
if (currentApiResult is bool isClanMember && isClanMember) return true;
// Compatibility fallback for older Clans implementations.
var playerClanTag = Clans.Call<string>("GetClanOf", playerId);
if (string.IsNullOrEmpty(playerClanTag)) return false;
var targetClanTag = Clans.Call<string>("GetClanOf", targetId);
if (string.IsNullOrEmpty(targetClanTag)) return false;
return playerClanTag == targetClanTag;
}
#endregion
#region Team
private bool InTeam(ulong playerId)
{
if (!RelationshipManager.TeamsEnabled()) return false;
var playersTeam = RelationshipManager.ServerInstance.FindPlayersTeam(playerId);
return playersTeam != null && playersTeam.members.Count > 1;
}
private List<ulong> GetTeamMembers(ulong playerId)
{
if (!RelationshipManager.TeamsEnabled()) return new List<ulong>();
var playersTeam = RelationshipManager.ServerInstance.FindPlayersTeam(playerId);
return playersTeam?.members;
}
private bool SameTeam(ulong playerId, ulong targetId)
{
if (!RelationshipManager.TeamsEnabled()) return false;
var playerTeam = RelationshipManager.ServerInstance.FindPlayersTeam(playerId);
if (playerTeam == null) return false;
var targetTeam = RelationshipManager.ServerInstance.FindPlayersTeam(targetId);
if (targetTeam == null) return false;
return playerTeam == targetTeam;
}
#endregion
#endregion
#region External Plugin Hooks
private void OnFriendAdded(string playerIdStr, string friendIdStr)
{
if (!ulong.TryParse(playerIdStr, out var playerId) || !ulong.TryParse(friendIdStr, out var friendId)) return;
ClearShareTargetCaches(playerId, friendId);
if (!config.ShareToExistingMembers && !config.ShareToNewMembers) return;
// Only proceed if both players consider each other friends
if (!AreMutualFriends(playerId, friendId))
{
DebugLog($"Not sharing blueprints between {GetPlayerName(playerId)} and {GetPlayerName(friendId)} — friendship is not mutual.");
return;
}
var player = RustCore.FindPlayerById(playerId);
var friend = RustCore.FindPlayerById(friendId);
if (player == null || friend == null) return;
DebugLog($"{player.displayName} added {friend.displayName} as a friend");
// Share FROM new friend TO player
if (config.ShareToExistingMembers)
{
ShareWithPlayer(friend, player); // player receives blueprints from friend
}
// Share FROM player TO new friend
if (config.ShareToNewMembers)
{
ShareWithPlayer(player, friend); // friend receives blueprints from player
}
}
private void OnFriendRemoved(string playerIdStr, string friendIdStr)
{
if (!ulong.TryParse(playerIdStr, out var playerId) || !ulong.TryParse(friendIdStr, out var friendId)) return;
ClearShareTargetCaches(playerId, friendId);
if (!config.LoseBlueprintsOnLeave) return;
DebugLog($"{GetPlayerName(playerId)} has removed {GetPlayerName(friendId)} as a friend");
var stillFriends = IsFriend(friendId, playerId);
if (stillFriends)
{
DebugLog($"{GetPlayerName(playerId)} removed {GetPlayerName(friendId)}, but they are still mutual friends. No blueprints removed.");
return;
}
// If not mutual anymore, remove shared blueprints in both directions
var playerToFriend = GetSharedBlueprints(playerIdStr, ShareType.Friends, friendIdStr);
var friendToPlayer = GetSharedBlueprints(friendIdStr, ShareType.Friends, playerIdStr);
if (playerToFriend.Count > 0)
{
RemoveBlueprints(playerId, playerToFriend, ShareType.Friends, friendIdStr);
}
if (friendToPlayer.Count > 0)
{
RemoveBlueprints(friendId, friendToPlayer, ShareType.Friends, playerIdStr);
}
}
private void OnClanMemberJoined(string tag, ulong joining, List<ulong> members)
{
HandleClanMemberJoined(tag, joining, members);
}
// Compatibility with older Clans hook signatures.
private void OnClanMemberJoined(ulong playerId, string clanName)
{
HandleClanMemberJoined(clanName, playerId, null);
}
private void OnClanMemberJoined(string tag, string joining, List<string> members)
{
if (!ulong.TryParse(joining, out var joiningId)) return;
HandleClanMemberJoined(tag, joiningId, members?.Select(member =>
{
ulong.TryParse(member, out var memberId);
return memberId;
}).Where(memberId => memberId != 0UL));
}
private void HandleClanMemberJoined(string clanName, ulong playerId, IEnumerable<ulong> memberIds)
{
var clanMemberIds = memberIds?.Where(memberId => memberId != 0UL).Distinct().ToList() ?? GetClanMembers(playerId);
ClearShareTargetCache(playerId);
ClearShareTargetCaches(clanMemberIds.ToArray());
if (!config.ShareToExistingMembers && !config.ShareToNewMembers) return;
var player = RustCore.FindPlayerById(playerId);
if (player == null) return;
DebugLog($"{player.displayName} joined the {clanName} clan");
if (clanMemberIds == null || clanMemberIds.Count == 0) return;
var clanMembers = FindPlayersByIds(clanMemberIds, playerId);
if (clanMembers.Count == 0) return;
foreach (var clanMember in clanMembers)
{
if (clanMember == null || clanMember == player) continue;
// Share FROM joining player TO existing member
if (config.ShareToExistingMembers)
{
ShareWithPlayer(player, clanMember); // clan member receives from new player
}
// Share FROM existing member TO joining player
if (config.ShareToNewMembers)
{
ShareWithPlayer(clanMember, player); // new player receives from clan member
}
}
}
private void OnClanMemberGone(string tag, ulong playerId, List<ulong> members)
{
HandleClanMemberGone(tag, playerId, members);
}
// Compatibility with older Clans hook signatures.
private void OnClanMemberGone(ulong playerId, string tag)
{
HandleClanMemberGone(tag, playerId, null);
}
private void OnClanMemberGone(string tag, string playerId, List<string> members)
{
if (!ulong.TryParse(playerId, out var parsedPlayerId)) return;
HandleClanMemberGone(tag, parsedPlayerId, members?.Select(member =>
{
ulong.TryParse(member, out var memberId);
return memberId;
}).Where(memberId => memberId != 0UL));
}
private void HandleClanMemberGone(string tag, ulong playerId, IEnumerable<ulong> memberIds)
{
ClearShareTargetCache(playerId);
ClearShareTargetCaches(memberIds?.ToArray() ?? Array.Empty<ulong>());
if (!config.LoseBlueprintsOnLeave) return;
var learntBlueprints = GetSharedBlueprints(playerId.ToString(), ShareType.Clans);
if (learntBlueprints.Count == 0) return;
DebugLog($"{GetPlayerName(playerId)} left the {tag} clan");
RemoveBlueprints(playerId, learntBlueprints, ShareType.Clans);
}
private void OnClanDisbanded(string tag, List<ulong> memberIds)
{
HandleClanDisbanded(memberIds?.Select(memberId => memberId.ToString()));
}
// Compatibility with older Clans hook signatures.
private void OnClanDisbanded(List<string> memberIds)
{
HandleClanDisbanded(memberIds);
}
private void OnClanDisbanded(string tag, List<string> memberIds)
{
HandleClanDisbanded(memberIds);
}
private void HandleClanDisbanded(IEnumerable<string> memberIds)
{
if (memberIds == null) return;
var members = memberIds.ToList();
ClearShareTargetCaches(members
.Select(memberId => ulong.TryParse(memberId, out var parsedMemberId) ? parsedMemberId : 0UL)
.Where(memberId => memberId != 0UL)
.ToArray());
if (!config.LoseBlueprintsOnLeave) return;
int processedPlayers = 0;
foreach (var memberId in members)
{
var sharedBlueprints = GetSharedBlueprints(memberId, ShareType.Clans);
if (sharedBlueprints == null || sharedBlueprints.Count == 0)
{
continue;
}
if (ulong.TryParse(memberId, out var playerId))
{
RemoveBlueprints(playerId, sharedBlueprints, ShareType.Clans);
processedPlayers++;
}
else
{
// Fallback: if we somehow can't parse the player id, at least clear our tracking data
RemoveBlueprintsFromDatabase(ShareType.Clans, memberId, string.Empty);
}
}
DebugLog($"Removed clan blueprints for {processedPlayers} player(s) on clan disband");
}
#endregion
#region Core
private bool TryShareBlueprint(ItemDefinition item, BasePlayer player)
{
if (item == null || player == null) return false;
if (!permission.UserHasPermission(player.UserIDString, usePermission)) return false;
if (BlueprintBlocked(item))
{
SendMessage("BlueprintBlocked", player, true, item.displayName.translated);
return false;
}
var playerId = player.userID;
if (SharingEnabled(player.UserIDString) && HasSocialConnections(playerId) && SomeoneWillLearnBlueprint(playerId, item))
{
var targetIds = GetCachedShareTargets(playerId);
ShareBlueprint(player, targetIds, item);
return true;
}
return false;
}
private static class ListPool<T>
{
private static readonly Stack<List<T>> pool = new Stack<List<T>>();
public static List<T> Get()
{
return pool.Count > 0 ? pool.Pop() : new List<T>();
}
public static void Free(List<T> list)
{
list.Clear();
pool.Push(list);
}
}
private class UnlockTask
{
public ulong TargetId;
public ulong SharerId;
public List<int> Blueprints = ListPool<int>.Get();
}
private bool QueueBlueprintUnlock(ulong playerId, ulong sharerId, int blueprintId, List<int> unlockQueue)
{
if (unlockQueue == null || unlockQueue.Contains(blueprintId)) return false;
var playerInfo = ServerMgr.Instance.persistance.GetPlayerInfo(playerId);
if (playerInfo == null) return false;
var unlockedItems = playerInfo.unlockedItems;
if (unlockedItems == null) return false;
if (unlockedItems.Contains(blueprintId)) return false;
unlockQueue.Add(blueprintId);
DebugLog($"Added {GetItemNameById(blueprintId)} to unlock queue for {GetPlayerName(playerId)}");
if (config.LoseBlueprintsOnLeave)
{
AddBlueprintToDatabase(playerId, sharerId, blueprintId);
}
return true;
}
private int ProcessQueuedBlueprintUnlocks(ulong playerId, ulong sharerId, List<int> unlockQueue)
{
if (unlockQueue.Count == 0) return 0;
var persistance = ServerMgr.Instance.persistance;
if (persistance == null) return 0;
var playerInfo = persistance.GetPlayerInfo(playerId);
if (playerInfo == null) return 0;
playerInfo.unlockedItems.AddRange(unlockQueue);
persistance.SetPlayerInfo(playerId, playerInfo);
var player = RustCore.FindPlayerById(playerId);
var isOnline = player != null && player.net?.connection != null && player.net.connection.connected;
if (isOnline)
{
foreach (var blueprint in unlockQueue)
{
if (!player.PersistantPlayerInfo.unlockedItems.Contains(blueprint))
{
player.PersistantPlayerInfo.unlockedItems.Add(blueprint);
}
}
player.ClientRPC(RpcTarget.Player("UnlockedBlueprint", player), 0);
player.stats.Add("blueprint_studied", unlockQueue.Count);
player.SendNetworkUpdateImmediate();
PlaySoundEffect(player);
}
else
{
AddPendingNotification(playerId, sharerId, unlockQueue);
}
DebugLog($"Unlocked {unlockQueue.Count} blueprint(s) for {GetPlayerName(playerId)}");
return unlockQueue.Count;
}
private void ShareBlueprint(BasePlayer sharer, List<ulong> targetIds, ItemDefinition item)
{
var blueprintId = item.itemid;
var sharedCount = 0;
var tasks = new List<UnlockTask>();
DebugLog($"{sharer.displayName} is Sharing {GetItemNameById(blueprintId)} with {targetIds.Count()} player(s)");
foreach (var targetId in targetIds)
{
if (targetId == sharer.userID) continue;
var task = new UnlockTask
{
TargetId = targetId,
SharerId = sharer.userID
};
var additionalUnlocks = item.Blueprint?.additionalUnlocks;
if (additionalUnlocks != null)
{
foreach (var blueprint in additionalUnlocks)
{
if (blueprint == null) continue;
QueueBlueprintUnlock(targetId, sharer.userID, blueprint.itemid, task.Blueprints);
}
}
QueueBlueprintUnlock(targetId, sharer.userID, blueprintId, task.Blueprints);
sharedCount++;
if (task.Blueprints.Count > 0)
{
tasks.Add(task);
}
else
{
ListPool<int>.Free(task.Blueprints);
}
}
foreach (var task in tasks)
{
ProcessQueuedBlueprintUnlocks(task.TargetId, task.SharerId, task.Blueprints);
var target = RustCore.FindPlayerById(task.TargetId);
if (target != null && config.ReceiveMessagesEnabled)
{
SendMessage("TargetLearntBlueprint", target, true, sharer.displayName, item.displayName.translated);
}
ListPool<int>.Free(task.Blueprints);
}
if (sharedCount > 0)
{
if (config.ShareMessagesEnabled)
{
SendMessage("PlayerSharedBlueprint", sharer, true, item.displayName.translated, sharedCount);
}
if (config.LoseBlueprintsOnLeave)
{
MarkDataDirty();
}
}
}
private void ShareWithPlayer(BasePlayer sharer, BasePlayer target)
{
if (sharer == null || target == null) return;
DebugLog($"{sharer.displayName} started sharing their blueprints with {target.displayName}");
var playerId = sharer.userID;
var targetId = target.userID;
if (!SharingEnabled(target.UserIDString))
{
SendMessage("TargetSharingDisabled", sharer, true, target.displayName);
return;
}
if (!SameTeam(playerId, targetId) && !SameClan(playerId, targetId) && !AreMutualFriends(playerId, targetId) && !permission.UserHasPermission(sharer.UserIDString, bypassPermission))
{
SendMessage("CannotShare", sharer, true);
return;
}
var filteredBlueprints = RemoveBlockedBlueprints(sharer.PersistantPlayerInfo.unlockedItems);
if (filteredBlueprints.Count == 0)
{
SendMessage("NoBlueprintsToShare", sharer, true, target.displayName);
return;
}
var queue = ListPool<int>.Get();
foreach (var blueprintId in filteredBlueprints)
{
QueueBlueprintUnlock(targetId, playerId, blueprintId, queue);
}
var unlocked = ProcessQueuedBlueprintUnlocks(targetId, playerId, queue);
ListPool<int>.Free(queue);
if (unlocked > 0)
{
if (config.LoseBlueprintsOnLeave)
{
MarkDataDirty();
}
if (config.ShareMessagesEnabled)
{
SendMessage("PlayerSharedBlueprints", sharer, true, unlocked, target.displayName);
}
if (config.ReceiveMessagesEnabled)
{
SendMessage("TargetLearntBlueprints", target, true, sharer.displayName, unlocked);
}
}
else
{
SendMessage("NoBlueprintsToShare", sharer, true, target.displayName);
}
}
private bool HasSocialConnections(ulong playerId)
{
return InTeam(playerId) || InClan(playerId) || HasFriends(playerId);
}
private List<int> RemoveBlockedBlueprints(List<int> blueprints)
{
return blueprints
.Select(ItemManager.FindItemDefinition)
.Where(item => !BlueprintBlocked(item))
.Select(item => item.itemid)
.ToList();
}
private bool SomeoneWillLearnBlueprint(ulong playerId, ItemDefinition item)
{
if (item == null) return false;
var targetIds = GetCachedShareTargets(playerId);
if (targetIds.Count == 0) return false;
var additionalUnlocks = item.Blueprint?.additionalUnlocks;
foreach (var targetId in targetIds)
{
if (PlayerWouldLearnBlueprint(targetId, item.itemid))
{
return true;
}
if (additionalUnlocks == null) continue;
foreach (var blueprintItem in additionalUnlocks)
{
if (blueprintItem == null) continue;
if (PlayerWouldLearnBlueprint(targetId, blueprintItem.itemid))
{
return true;
}
}
}
return false;
}
private bool PlayerWouldLearnBlueprint(ulong playerId, int blueprintId)
{
var playerInfo = ServerMgr.Instance.persistance.GetPlayerInfo(playerId);
if (playerInfo == null) return false;
var unlockedItems = playerInfo.unlockedItems;
if (unlockedItems.Contains(blueprintId)) return false;
return true;
}
private List<ulong> GetPlayerIdsToShareWith(ulong playerId)
{
var ids = new HashSet<ulong>();
if (isClansEnabled && InClan(playerId))
{
var clanMembers = GetClanMembers(playerId);
if (clanMembers != null)
{
ids.UnionWith(clanMembers);
}
}
if (isFriendsEnabled && HasFriends(playerId))
{
var mutualFriends = GetFriends(playerId).Where(friendId => AreMutualFriends(playerId, friendId));
ids.UnionWith(mutualFriends);
}
if (isTeamsEnabled && InTeam(playerId))
{
var teamMembers = GetTeamMembers(playerId);
if (teamMembers != null)
{
ids.UnionWith(teamMembers);
}
}
ids.Remove(playerId);
return ids.ToList();
}
private List<ulong> GetCachedShareTargets(ulong playerId)
{
if (shareTargetCache == null)
{
shareTargetCache = new Dictionary<ulong, List<ulong>>();
}
if (shareTargetCache.TryGetValue(playerId, out var cached))
{
return cached;
}
var targetIds = GetPlayerIdsToShareWith(playerId);
if (targetIds == null)
{
targetIds = Enumerable.Empty<ulong>().ToList();
}
var freshTargets = new List<ulong>();
foreach (var targetId in targetIds)
{
if (SharingEnabled(targetId.ToString()))
{
freshTargets.Add(targetId);
}
}
shareTargetCache[playerId] = freshTargets;
if (shareTargetCacheTimers.TryGetValue(playerId, out var oldTimer))
{
oldTimer.Destroy();
shareTargetCacheTimers.Remove(playerId);
}
shareTargetCacheTimers[playerId] = timer.Once(shareTargetCacheDuration, () =>
{
shareTargetCache.Remove(playerId);
shareTargetCacheTimers.Remove(playerId);
});
return freshTargets;
}
private void ClearShareTargetCache(ulong playerId)
{
if (shareTargetCache == null) return;
shareTargetCache.Remove(playerId);
if (shareTargetCacheTimers.TryGetValue(playerId, out var existingTimer))
{
existingTimer.Destroy();
shareTargetCacheTimers.Remove(playerId);
}
}
private void ClearShareTargetCaches(params ulong[] playerIds)
{
if (playerIds == null || playerIds.Length == 0) return;
foreach (var playerId in playerIds)
{
if (playerId == 0) continue;
ClearShareTargetCache(playerId);
}
}
private void AddBlueprintToDatabase(ulong playerId, ulong sharerId, int blueprint)
{
var playerIdStr = playerId.ToString();
var sharerIdStr = sharerId.ToString();
var changed = false;
if (isTeamsEnabled && SameTeam(playerId, sharerId))
{
var list = GetSharedBlueprints(playerIdStr, ShareType.Teams);
if (!list.Contains(blueprint))
{
list.Add(blueprint);
changed = true;
DebugLog($"Added {GetItemNameById(blueprint)} to team blueprints for {GetPlayerName(playerId)}");
}
}
if (isClansEnabled && SameClan(playerId, sharerId))
{
var list = GetSharedBlueprints(playerIdStr, ShareType.Clans);
if (!list.Contains(blueprint))
{
list.Add(blueprint);
changed = true;
DebugLog($"Added {GetItemNameById(blueprint)} to clan blueprints for {GetPlayerName(playerId)}");
}
}
if (isFriendsEnabled && IsFriend(sharerId, playerId))
{
var list = GetSharedBlueprints(playerIdStr, ShareType.Friends, sharerIdStr);
if (!list.Contains(blueprint))
{
list.Add(blueprint);
changed = true;
DebugLog($"Added {GetItemNameById(blueprint)} to friend {GetPlayerName(sharerId)} blueprints for {GetPlayerName(playerId)}");
}
}
if (changed)
{
MarkDataDirty();
}
}
private void RemoveBlueprintsFromDatabase(ShareType type, string playerId, string friendId)
{
var list = GetSharedBlueprints(playerId, type, friendId);
list.Clear();
MarkDataDirty();
DebugLog($"Removed {type} blueprints for {GetPlayerName(ulong.Parse(playerId))} from database");
}
private void RemoveBlueprints(ulong playerId, HashSet<int> blueprintIds, ShareType type, string friendId = "")
{
if (blueprintIds == null || blueprintIds.Count == 0) return;
var playerInfo = ServerMgr.Instance.persistance.GetPlayerInfo(playerId);
if (playerInfo == null) return;
var player = RustCore.FindPlayerById(playerId);
int blueprintsRemoved = 0;
foreach (var blueprintId in blueprintIds)
{
if (!playerInfo.unlockedItems.Contains(blueprintId)) continue;
playerInfo.unlockedItems.Remove(blueprintId);
blueprintsRemoved++;
DebugLog($"Removed blueprint {GetItemNameById(blueprintId)} from {GetPlayerName(playerId)}");
// If player is online, remove from their in-memory list too
if (player != null && player.PersistantPlayerInfo.unlockedItems.Contains(blueprintId))
{
player.PersistantPlayerInfo.unlockedItems.Remove(blueprintId);
}
}
if (blueprintsRemoved == 0) return;
ServerMgr.Instance.persistance.SetPlayerInfo(playerId, playerInfo);
RemoveBlueprintsFromDatabase(type, playerId.ToString(), friendId);
if (player != null)
{
player.SendNetworkUpdateImmediate();
SendMessage("BlueprintsRemoved", player, true, blueprintsRemoved);
}
}
private void PlayerLeftTeam(ulong playerId)
{
if (!config.LoseBlueprintsOnLeave) return;
var sharedBlueprints = GetSharedBlueprints(playerId.ToString(), ShareType.Teams);
if (sharedBlueprints.Count == 0) return;
RemoveBlueprints(playerId, sharedBlueprints, ShareType.Teams);
}
private void AddPendingNotification(ulong playerId, ulong sharerId, List<int> blueprints)
{
if (!config.ReceiveMessagesEnabled || blueprints == null || blueprints.Count == 0) return;
if (isTeamsEnabled && SameTeam(playerId, sharerId))
{
AddPendingNotificationType(playerId, blueprints, ShareType.Teams);
}
else if (isClansEnabled && SameClan(playerId, sharerId))
{
AddPendingNotificationType(playerId, blueprints, ShareType.Clans);
}
else if (isFriendsEnabled && IsFriend(sharerId, playerId))
{
AddPendingNotificationType(playerId, blueprints, ShareType.Friends);
}
}
private void AddPendingNotificationType(ulong playerId, IEnumerable<int> blueprintIds, ShareType shareType)
{
var playerData = GetPlayerData(playerId.ToString());
if (playerData.PendingNotifications == null)
{
playerData.PendingNotifications = new PendingNotificationsData();
}
var target = shareType switch
{
ShareType.Teams => playerData.PendingNotifications.Team,
ShareType.Clans => playerData.PendingNotifications.Clan,
ShareType.Friends => playerData.PendingNotifications.Friend,
_ => null
};
if (target == null) return;
var added = false;
foreach (var blueprintId in blueprintIds)
{
if (target.Contains(blueprintId)) continue;
target.Add(blueprintId);
added = true;
}
if (added)
{
MarkDataDirty();
}
}
private void DisplayPendingNotifications(BasePlayer player)
{
if (!config.ReceiveMessagesEnabled || player == null || !player.IsConnected) return;
var pending = GetPlayerData(player.UserIDString).PendingNotifications;
if (pending == null) return;
pending.InitializeAfterLoad();
var notifications = new Dictionary<ShareType, List<int>>
{
[ShareType.Teams] = pending.Team,
[ShareType.Clans] = pending.Clan,
[ShareType.Friends] = pending.Friend
};
var totalBlueprints = notifications.Sum(entry => entry.Value.Count);
// Stay quiet when nothing was shared while the player was offline.
if (totalBlueprints == 0) return;
SendMessage("PendingNotifications", player, true, totalBlueprints);
foreach (var entry in notifications)
{
if (entry.Value.Count == 0) continue;
var blueprintNames = entry.Value
.Select(GetItemNameById)
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
if (blueprintNames.Count > 0)
{
SendMessage("ShowOfflineBlueprints", player, false, GetShareTypeLabel(entry.Key), string.Join(", ", blueprintNames));
}
entry.Value.Clear();
}
MarkDataDirty();
}
private static string GetShareTypeLabel(ShareType shareType)
{
return shareType switch
{
ShareType.Teams => "Team",
ShareType.Clans => "Clan",
ShareType.Friends => "Friend",
_ => shareType.ToString()
};
}
#endregion
#region Utility
private void PlaySoundEffect(BasePlayer player)
{
if (player == null) return;
var soundEffect = new Effect("assets/prefabs/deployable/research table/effects/research-success.prefab", player.transform.position, Vector3.zero);
if (soundEffect == null) return;
EffectNetwork.Send(soundEffect, player.net.connection);
}
private List<BasePlayer> FindPlayersByIds(IEnumerable<ulong> ids, ulong excludeId = 0)
{
return ids
.Where(id => id != excludeId)
.Select(RustCore.FindPlayerById)
.Where(player => player != null)
.Distinct()
.ToList();
}
private bool TryGetOtherPlayer(BasePlayer sender, string name, out BasePlayer target)
{
target = RustCore.FindPlayerByName(name);
if (target == null)
{
SendMessage("PlayerNotFound", sender, true);
return false;
}
if (target == sender)
{
SendMessage("TargetEqualsPlayer", sender, true);
return false;
}
return true;
}
private bool BlueprintBlocked(ItemDefinition item) => item == null || config.BlockedItems.Contains(item.shortname);
private Dictionary<int, List<string>> GroupBlueprintsByWorkbenchLevel(HashSet<int> sharedBlueprints)
{
var groupedBlueprints = new Dictionary<int, List<string>>();
foreach (var item in sharedBlueprints.Select(ItemManager.FindItemDefinition).Where(i => i != null))
{
var tier = item.Blueprint.workbenchLevelRequired;
if (!groupedBlueprints.TryGetValue(tier, out var list))
{
list = Pool.Get<List<string>>();
groupedBlueprints[tier] = list;
}
list.Add(item.displayName.translated);
}
return groupedBlueprints;
}
private void DisplayLearntBlueprints(BasePlayer player, ShareType type, string friendId = "")
{
if (player == null) return;
var sharedBlueprints = GetSharedBlueprints(player.UserIDString, type, friendId);
if (sharedBlueprints == null || sharedBlueprints.Count == 0)
{
SendMessage("NoSharedBlueprints", player, true);
return;
}
var groupedBlueprints = GroupBlueprintsByWorkbenchLevel(sharedBlueprints);
SendMessage("SharedBlueprintsTitle", player, true);
foreach (var kvp in groupedBlueprints.OrderBy(k => k.Key))
{
var tier = kvp.Key;
var names = kvp.Value;
var msg = GetLangValue($"ShowSharedBlueprints", player.UserIDString, tier.ToString(), string.Join(", ", names));
player.ChatMessage(msg);
}
FreeGroupedBlueprints(groupedBlueprints);
}
private void FreeGroupedBlueprints(Dictionary<int, List<string>> grouped)
{
foreach (var key in grouped.Keys.ToList())
{
var list = grouped[key];
Pool.FreeUnmanaged(ref list);
grouped[key] = null;
}
}
private void DebugLog(string message, DebugLevel level = DebugLevel.Info)
{
if (!config.Debug) return;
string prefixColor = level switch
{
DebugLevel.Warning => "#f1c40f",// yellow
DebugLevel.Error => "#e74c3c",// red
_ => "#3498db",// blue
};
PrintToConsole($"<color={prefixColor}>[BlueprintShare Debug]</color> {message}");
}
private string GetItemNameById(int itemId)
{
return ItemManager.FindItemDefinition(itemId)?.displayName?.translated ?? string.Empty;
}
private string GetPlayerName(ulong playerId)
{
return RustCore.FindPlayerById(playerId)?.displayName
?? covalence.Players.FindPlayerById(playerId.ToString())?.Name
?? "Unknown";
}
#endregion
#region Chat Commands
[ChatCommand("bs")]
private void ChatCommands(BasePlayer player, string command, string[] args)
{
if (args.Length == 0)
{
HelpCommand(player, args);
return;
}
var key = args[0].ToLower();
if (chatCommandHandlers.TryGetValue(key, out var handler))
{
handler(player, args);
}
else
{
SendMessage("ArgumentsError", player, true);
}
}
private void ToggleCommand(BasePlayer player, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, togglePermi
// ... [Code preview truncated due to length. Full source in attached file] ...