C#:
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using Oxide.Plugins;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Networking;
using Random = UnityEngine.Random;
using Time = UnityEngine.Time;
namespace Oxide.Plugins {
[Info("Roaming NPC Vendors", "k1lly0u", "0.1.6")]
class RoamingNPCVendors : RustPlugin {
#region Oxide Hooks
private readonly List<RoamingVendor> activeVendors = new List<RoamingVendor>();
private static Action<BasePlayer, string, Vector3> Message;
private const string ADMIN_PERMISSION = "roamingnpcvendors.admin";
private const TerrainTopology.Enum PREFERRED_TOPOLOGY = TerrainTopology.Enum.Roadside;
private const TerrainTopology.Enum BLOCKED_TOPOLOGY = TerrainTopology.Enum.Road;
private void OnServerInitialized() {
Message = ChatMessage;
permission.RegisterPermission(ADMIN_PERMISSION, this);
WalkablePath.GeneratePathMap(SpawnVendors);
}
protected override void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
["OnNPCAttacked"]
= "<size=20> Don't shoot, I'm friendly!</size>"
}
, this);
}
private object CanBradleyApcTarget(BradleyAPC bradleyAPC, RoamingVendor roamingVendor) => false;
private void OnEntityKill(RoamingVendor roamingVendor) {
if (roamingVendor != null) activeVendors.Remove(roamingVendor);
}
private void Unload() {
foreach (BasePlayer player in BasePlayer.activePlayerList) CuiHelper.DestroyUi(player, PaginatedVendingMachine.UI_PANEL);
for (int i = 0; i < activeVendors.Count; i++) {
RoamingVendor roamingVendor = activeVendors[i];
if (roamingVendor == null || roamingVendor.IsDestroyed) continue;
roamingVendor.inventory.Strip();
roamingVendor.machine.inventory.Clear();
roamingVendor.machine.Kill();
roamingVendor.Kill();
}
activeVendors.Clear();
PaginatedVendingMachine.ActiveVendingPlayers.Clear();
Message = null;
Configuration = null;
}
#endregion
#region Functions
private static void CopySerializeableFields<T>(T src, T dst) {
FieldInfo[] srcFields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (FieldInfo field in srcFields) {
object value = field.GetValue(src);
field.SetValue(dst, value);
}
}
private void ChatMessage(BasePlayer player, string key, Vector3 position) => player.SendConsoleCommand("ddraw.text", 10, Color.red, position, lang.GetMessage(key, this, player.UserIDString));
private void SpawnVendors() {
if (WalkablePath._allPaths.Count == 0) {
Debug.LogError($"[Roaming NPC Vendors] No paths found on map");
return;
}
for (int i = 0; i < Configuration.Vendors.Count; i++) {
if (!Configuration.Vendors[i].Enabled) continue;
Vector3 position;
SpawnVendor(Configuration.Vendors[i], out position);
}
}
private bool SpawnVendor(ConfigData.Vendor vendor, out Vector3 position) {
position = Vector3.zero;
if (WalkablePath._allPaths.Count == 0) return false;
WalkablePath walkablePath = WalkablePath.GetRandom();
for (int y = 0; y < 10; y++) {
int positionIndex;
position = walkablePath.GetRandomPosition(out positionIndex);
if (NavmeshSpawnPoint.Find(position, 10f, out position)) {
RoamingVendor roamingVendor = InstantiateTrader(position, vendor);
roamingVendor.WalkablePath = walkablePath;
roamingVendor.PathIndex = positionIndex;
roamingVendor.Spawn();
activeVendors.Add(roamingVendor);
Debug.Log($"[Roaming NPC Vendors] Spawned {vendor.VendorName} at {position}");
return true;
}
}
return false;
}
#endregion
#region Pathing
public class HapisFixedPath : WalkablePath {
private List<Vector3> nodes;
public HapisFixedPath(Action callback) : base() {
ServerMgr.Instance.StartCoroutine(DownloadHapisFixedPath(callback));
}
public override Vector3 GetNextNode(ref WalkablePath path, ref int currentIndex, ref bool isReversed) {
currentIndex++;
if (currentIndex >= nodes.Count) currentIndex = 0;
return nodes[currentIndex];
}
public override Vector3 GetRandomPosition(out int index) {
index = Random.Range(0, nodes.Count);
return nodes[index];
}
public IEnumerator DownloadHapisFixedPath(Action callback) {
Debug.Log("[Roaming NPC Vendors] Downloading Hapis fixed path...");
const string GITHUB_URL = "https://github.com/k1lly0u/RoamingNPCVendors-Hapis/raw/main/hapis-fixed-path.json";
UnityWebRequest request = UnityWebRequest.Get(GITHUB_URL);
yield return request.SendWebRequest();
if (request.isNetworkError || request.isHttpError) {
Debug.Log($"[Roaming NPC Vendors] Failed to download Hapis path");
request.Dispose();
yield break;
}
nodes = JsonConvert.DeserializeObject<List<Vector3>>(request.downloadHandler.text);
request.Dispose();
callback.Invoke();
}
}
public class WalkablePath {
public static readonly List<WalkablePath> _allPaths = new List<WalkablePath>();
private readonly PathList pathList;
private Hash<int, ConnectedPath> intersections;
private bool IsTrailPath {
get;
set;
}
private float PathOffset {
get;
set;
}
public static void GeneratePathMap(Action callback) {
if (ConVar.Server.level == "HapisIsland") {
if (_allPaths.Count == 0) _allPaths.Add(new HapisFixedPath(callback));
else callback.Invoke();
}
else {
if (_allPaths.Count == 0) {
for (int i = 0; i < TerrainMeta.Path.Roads.Count; i++) _allPaths.Add(new WalkablePath(TerrainMeta.Path.Roads[i]));
for (int i = 0; i < _allPaths.Count; i++) _allPaths[i].FindIntersections();
for (int i = _allPaths.Count - 1; i >= 0; i--) {
if (!_allPaths[i].HasIntersections()) _allPaths.RemoveAt(i);
}
Debug.Log($"[Roaming NPC Vendors] Generated path map with {_allPaths.Count} connected paths");
}
else Debug.Log($"[Roaming NPC Vendors] Using previously generated path map with {_allPaths.Count} connected paths");
callback.Invoke();
}
}
public WalkablePath() {
}
public WalkablePath(PathList pathList) {
this.pathList = pathList;
IsTrailPath = pathList.Width < 5f;
PathOffset = pathList.Width * 0.6f;
}
public void FindIntersections() {
Vector3 startPoint = pathList.Path.GetStartPoint();
Vector3 endPoint = pathList.Path.GetEndPoint();
for (int y = 0; y < _allPaths.Count; y++) {
WalkablePath walkablePath = _allPaths[y];
if (walkablePath == this) continue;
for (int i = 0; i < walkablePath.pathList.Path.Points.Length; i++) {
Vector3 n = walkablePath.pathList.Path.Points[i];
if (Vector3.Distance(startPoint, n) < 6f) {
RegisterIntersection(0, walkablePath, i);
walkablePath.RegisterIntersection(i, this, 0);
}
if (Vector3.Distance(endPoint, n) < 6f) {
RegisterIntersection(pathList.Path.Points.Length - 1, walkablePath, i);
walkablePath.RegisterIntersection(i, this, pathList.Path.Points.Length - 1);
}
}
}
}
public bool HasIntersections() => intersections != null && intersections.Count > 0;
private void RegisterIntersection(int index, WalkablePath connectedPath, int connectedIndex) {
if (intersections == null) intersections = new Hash<int, ConnectedPath>();
intersections[index] = new ConnectedPath(connectedPath, connectedIndex);
}
private bool HasIntersection(int index, out ConnectedPath connectedPath) {
if (intersections == null) {
connectedPath = default(ConnectedPath);
return false;
}
return intersections.TryGetValue(index, out connectedPath);
}
public virtual Vector3 GetNextNode(ref WalkablePath path, ref int currentIndex, ref bool isReversed) {
PathList pathList = this.pathList;
ConnectedPath connectedPath;
if (HasIntersection(currentIndex, out connectedPath)) {
if ((currentIndex == 0 || currentIndex >= pathList.Path.Points.Length - 1) || (currentIndex > 0 && currentIndex < pathList.Path.Points.Length - 1 && Random.Range(0f, 1f) <= 0.5f)) {
path = connectedPath.walkablePath;
currentIndex = connectedPath.connectedIndex;
pathList = path.pathList;
isReversed = currentIndex >= pathList.Path.Points.Length - 1 ? true : Random.Range(0f, 1f) > 0.5f;
}
}
if (isReversed) {
if (currentIndex == 0) {
if (pathList.Path.Circular) {
currentIndex = pathList.Path.Points.Length - 1;
}
else {
isReversed = false;
currentIndex = Mathf.Clamp(currentIndex + 1, 0, pathList.Path.Points.Length - 1);
}
}
else currentIndex = Mathf.Clamp(currentIndex - 1, 0, pathList.Path.Points.Length - 1);
}
else {
if (currentIndex == pathList.Path.Points.Length - 1) {
if (pathList.Path.Circular) {
currentIndex = 0;
}
else {
isReversed = true;
currentIndex = Mathf.Clamp(currentIndex - 1, 0, pathList.Path.Points.Length - 1);
}
}
else currentIndex = Mathf.Clamp(currentIndex + 1, 0, pathList.Path.Points.Length - 1);
}
if (IsTrailPath) return pathList.Path.Points[currentIndex];
else {
Vector3 currentNode = isReversed ? pathList.Path.Points[Mathf.Clamp(currentIndex + 1, 0, pathList.Path.Points.Length - 1)] : pathList.Path.Points[Mathf.Clamp(currentIndex - 1, 0, pathList.Path.Points.Length - 1)];
Vector3 nextNode = pathList.Path.Points[currentIndex];
Quaternion rotation = Quaternion.LookRotation((currentNode - nextNode).normalized, Vector3.up);
return nextNode + (rotation * (Vector3.right * PathOffset));
}
}
public static WalkablePath GetRandom() => _allPaths.GetRandom();
public virtual Vector3 GetRandomPosition(out int index) {
index = Random.Range(0, pathList.Path.Points.Length);
return pathList.Path.Points[index];
}
private struct ConnectedPath {
public WalkablePath walkablePath;
public int connectedIndex;
public ConnectedPath(WalkablePath walkablePath, int connectedIndex) {
this.walkablePath = walkablePath;
this.connectedIndex = connectedIndex;
}
}
}
#endregion
#region NPC Spawning
private static RoamingVendor _vendorReference;
private static RoamingVendor VendorReference {
get {
if (_vendorReference == null) {
const string BANDIT_SHOPKEEPER = "assets/prefabs/npc/bandit/shopkeepers/bandit_shopkeeper.prefab";
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab(BANDIT_SHOPKEEPER), Vector3.zero, Quaternion.identity);
gameObject.name = BANDIT_SHOPKEEPER;
gameObject.SetActive(false);
NPCShopKeeper npcShopKeeper = gameObject.GetComponent<NPCShopKeeper>();
RoamingVendor roamingVendor = gameObject.AddComponent<RoamingVendor>();
CopySerializeableFields<NPCShopKeeper>(npcShopKeeper, roamingVendor);
NPCPlayerNavigator navigator = gameObject.AddComponent<NPCPlayerNavigator>();
navigator.Acceleration = 8f;
navigator.Speed = 6.2f;
navigator.TurnSpeed = 120;
navigator.DefaultArea = "Walkable";
navigator.topologyPreference = TerrainTopology.Enum.Roadside;
VendorBrain brain = gameObject.AddComponent<VendorBrain>();
brain.SenseTypes = EntityType.Player;
brain.MaxGroupSize = int.MaxValue;
brain.AttackRangeMultiplier = 1f;
brain.SenseRange = 5;
brain.ListenRange = 5;
brain.TargetLostRange = 5;
brain.CheckVisionCone = true;
brain.IgnoreNonVisionSneakers = true;
brain.IgnoreSafeZonePlayers = true;
brain.VisionCone = Vector3.Dot(Vector3.forward, Quaternion.Euler(0f, 135f, 0f) * Vector3.forward);
brain.UseAIDesign = true;
brain.Pet = false;
NavMeshAgent navAgent = gameObject.AddComponent<NavMeshAgent>();
navAgent.stoppingDistance = 0.25f;
navAgent.autoBraking = true;
navAgent.autoRepath = true;
navAgent.agentTypeID = 0;
navAgent.areaMask = 1;
navAgent.agentTypeID = -1372625422;
navAgent.speed = 3.5f;
navAgent.angularSpeed = 120;
navAgent.acceleration = 8;
navAgent.radius = 0.5f;
navAgent.height = 2;
navAgent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
navAgent.avoidancePriority = 50;
navAgent.updatePosition = true;
navAgent.updateRotation = true;
navAgent.updateUpAxis = true;
roamingVendor.enableSaving = false;
UnityEngine.Object.DestroyImmediate(npcShopKeeper);
_vendorReference = roamingVendor;
}
return _vendorReference;
}
}
private static RoamingVendor InstantiateTrader(Vector3 position, ConfigData.Vendor config) {
RoamingVendor roamingVendor = UnityEngine.Object.Instantiate<RoamingVendor>(VendorReference, position, Quaternion.identity);
roamingVendor.name = VendorReference.name;
roamingVendor.Config = config;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(roamingVendor.gameObject, Rust.Server.EntityScene);
roamingVendor.gameObject.SetActive(true);
return roamingVendor;
}
private static class NavmeshSpawnPoint {
private static NavMeshHit navmeshHit;
private static RaycastHit raycastHit;
private static readonly Collider[] _buffer = new Collider[256];
private const int WORLD_LAYER = 65536;
public static bool Find(Vector3 targetPosition, float maxDistance, out Vector3 position) {
for (int i = 0; i < 10; i++) {
position = i == 0 ? targetPosition : targetPosition + (Random.onUnitSphere * maxDistance);
if (NavMesh.SamplePosition(position, out navmeshHit, maxDistance, 1)) {
if (IsInRockPrefab(navmeshHit.position)) continue;
if (IsNearWorldCollider(navmeshHit.position)) continue;
if (navmeshHit.position.y < TerrainMeta.WaterMap.GetHeight(navmeshHit.position)) continue;
position = navmeshHit.position;
return true;
}
}
position = default(Vector3);
return false;
}
private static bool IsInRockPrefab(Vector3 position) {
Physics.queriesHitBackfaces = true;
bool isInRock = Physics.Raycast(position, Vector3.up, out raycastHit, 20f, WORLD_LAYER, QueryTriggerInteraction.Ignore) && BLOCKED_COLLIDERS.Any(s => raycastHit.collider?.gameObject?.name.Contains(s, CompareOptions.OrdinalIgnoreCase) ?? false);
Physics.queriesHitBackfaces = false;
return isInRock;
}
private static bool IsNearWorldCollider(Vector3 position) {
Physics.queriesHitBackfaces = true;
int count = Physics.OverlapSphereNonAlloc(position, 2f, _buffer, WORLD_LAYER, QueryTriggerInteraction.Ignore);
Physics.queriesHitBackfaces = false;
int removed = 0;
for (int i = 0; i < count; i++) {
if (ACCEPTED_COLLIDERS.Any(s => _buffer[i].gameObject.name.Contains(s, CompareOptions.OrdinalIgnoreCase))) removed++;
}
return count - removed > 0;
}
private static readonly string[] ACCEPTED_COLLIDERS = new string[] {
"road", "carpark", "rocket_factory", "range", "train_track", "runway", "_grounds", "concrete_slabs", "lighthouse", "cave", "office", "walkways", "sphere", "tunnel", "industrial", "junkyard"
}
;
private static readonly string[] BLOCKED_COLLIDERS = new string[] {
"rock", "cliff", "junk", "range", "invisible"
}
;
}
#endregion
#region NPC Components
public class RoamingVendor : NPCShopKeeper, IAISenses, IAIAttack, IThinker {
private float lastThinkTime;
private VendingMachineMapMarker mapMarker;
public WalkablePath WalkablePath;
public int PathIndex;
public bool IsPanicked {
get;
private set;
}
public ConfigData.Vendor Config {
get;
set;
}
public Transform Transform {
get;
private set;
}
public VendorBrain Brain {
get;
private set;
}
private void Awake() {
Transform = transform;
}
public override void ServerInit() {
loadouts = null;
machine = InstantiateVendingMachine(Transform.position);
(machine as PaginatedVendingMachine).Vendor = this;
machine.syncPosition = true;
machine.enableSaving = false;
machine.Spawn();
mapMarker = typeof(VendingMachine).GetField("myMarker", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(machine) as VendingMachineMapMarker;
if (!Configuration.ShowMapMarker) machine.SetFlag(Flags.Reserved4, false);
LegacyNavigation = false;
base.ServerInit();
Config.ApplyClothing(this);
Brain = gameObject.GetComponent<VendorBrain>();
AIThinkManager.Add(this);
InvokeRandomized(UpdateVendingPositionRotation, 1f, 1f, 0.1f);
Invoke(SetDisplayName, 1f);
}
private void SetDisplayName() => displayName = Config.VendorName;
private void UpdateVendingPositionRotation() {
machine.transform.position = Transform.position;
machine.transform.rotation = Quaternion.Euler(0f, eyes.rotation.eulerAngles.y, 0f);
machine.SendNetworkUpdate_Position();
if (mapMarker != null && !mapMarker.IsDestroyed) {
mapMarker.transform.position = Transform.position;
mapMarker.SendNetworkUpdate_Position();
}
}
public override void Hurt(HitInfo info) {
BasePlayer player = info.InitiatorPlayer;
if (player != null && (player != lastAttacker || Time.time - lastAttackedTime > 10f)) {
lastAttackedTime = Time.time;
Message(player, "OnNPCAttacked", eyes.position + (Vector3.up * 0.1f));
lastAttacker = player;
IsPanicked = true;
CancelInvoke(CancelPanic);
Invoke(CancelPanic, 30f);
}
}
private void CancelPanic() => IsPanicked = false;
#region IThinker
public override void ServerThink(float delta) {
base.ServerThink(delta);
if (Brain.ShouldServerThink()) Brain.DoThink();
}
public void TryThink() {
ServerThink(Time.time - lastThinkTime);
lastThinkTime = Time.time;
}
#endregion
#region IAISenses
public bool IsFriendly(BaseEntity entity) => !IsPanicked;
public bool IsThreat(BaseEntity entity) => IsTarget(entity);
public bool IsTarget(BaseEntity entity) {
if (IsPanicked) return false;
BasePlayer player = entity as BasePlayer;
if (player != null && player.IsConnected && !player.IsSleeping()) return true;
return false;
}
#endregion
#region IAIAttack
public void AttackTick(float delta, BaseEntity target, bool targetIsLOS) {
}
public BaseEntity GetBestTarget() {
BaseEntity baseEntity = null;
float delta = -1f;
if (IsPanicked) return baseEntity;
foreach (BaseEntity player in Brain.Senses.Players) {
if (player == null || player.Health() <= 0f || player.IsNpc) continue;
if (!(player is BasePlayer) || !(player as BasePlayer).IsConnected || (player as BasePlayer).IsSleeping()) continue;
float distanceToTarget = Vector3.Distance(player.transform.position, Transform.position);
if (distanceToTarget > Brain.SenseRange) continue;
float rangeDelta = 1f - Mathf.InverseLerp(1f, Brain.SenseRange, distanceToTarget);
float dot = Vector3.Dot((player.transform.position - eyes.position).normalized, eyes.BodyForward());
rangeDelta += Mathf.InverseLerp(Brain.VisionCone, 1f, dot) / 2f;
rangeDelta += (Brain.Senses.Memory.IsLOS(player) ? 2f : 0f);
if (rangeDelta <= delta) continue;
baseEntity = player;
delta = rangeDelta;
}
return baseEntity;
}
public bool CanAttack(BaseEntity entity) => false;
public float EngagementRange() => 5f;
public bool IsTargetInRange(BaseEntity entity, out float dist) {
dist = Vector3.Distance(entity.transform.position, Transform.position);
BasePlayer player = entity as BasePlayer;
if (player == null || !player.IsConnected || player.IsSleeping()) return false;
return IsPanicked ? false : dist <= EngagementRange();
}
public bool CanSeeTarget(BaseEntity entity) {
if (IsPanicked) return false;
BasePlayer player = entity as BasePlayer;
if (player == null || !player.IsConnected || player.IsSleeping()) return false;
if (Vector3.Distance(entity.transform.position, Transform.position) > 5f) return false;
return IsPlayerVisibleToUs(player, 1218519041);
}
public float GetAmmoFraction() => 0f;
public bool NeedsToReload() => false;
public bool Reload() => false;
public float CooldownDuration() => 5f;
public bool IsOnCooldown() => false;
public bool StartAttacking(BaseEntity entity) => false;
public void StopAttacking() {
}
#endregion
}
public class VendorBrain : BaseAIBrain {
public override void InitializeAI() {
RoamingVendor entity = GetBaseEntity() as RoamingVendor;
entity.HasBrain = true;
Navigator = GetComponent<BaseNavigator>();
AIDesign = new AIDesign();
AIDesign.SetAvailableStates(GetStateList());
if (Events == null) Events = new AIEvents();
Senses.Init(entity, this, MemoryDuration, SenseRange, TargetLostRange, VisionCone, CheckVisionCone, CheckLOS, IgnoreNonVisionSneakers, ListenRange, HostileTargetsOnly, false, IgnoreSafeZonePlayers, SenseTypes, RefreshKnownLOS);
Events.Memory.Position.Set(transform.position, 4);
LoadAIDesign(ProtoBuf.AIDesign.Deserialize(DefaultDesign), null, 0);
AIInformationZone forPoint = AIInformationZone.GetForPoint(transform.position, false);
if (forPoint != null) forPoint.RegisterSleepableEntity(this);
BaseEntity.Query.Server.AddBrain(entity);
CancelInvoke(TickMovement);
InvokeRandomized(TickMovement, 1f, 0.1f, 0.0100000007f);
ThinkMode = AIThinkMode.Interval;
thinkRate = 0.25f;
PathFinder = new HumanPathFinder();
((HumanPathFinder)PathFinder).Init(entity);
}
public override void AddStates() {
states = new Dictionary<AIState, BasicAIState>();
AddState(new BaseIdleState());
AddState(new RoamState());
AddState(new InteractionState());
}
public class RoamState : BasicAIState {
private StateStatus status = StateStatus.Error;
private bool isReversed;
private static readonly Vector3[] preferedTopologySamples = new Vector3[4];
private static readonly Vector3[] topologySamples = new Vector3[4];
public RoamState() : base(AIState.Roam) {
}
public override void StateEnter(BaseAIBrain brain, BaseEntity entity) {
base.StateEnter(brain, entity);
status = StateStatus.Error;
if (brain.PathFinder == null) return;
RoamingVendor vendor = entity as RoamingVendor;
Vector3 bestRoamPosition = vendor.WalkablePath.GetNextNode(ref vendor.WalkablePath, ref vendor.PathIndex, ref isReversed);
if (brain.Navigator.GetNearestNavmeshPosition(bestRoamPosition, out bestRoamPosition, 20f) && brain.Navigator.IsAcceptableWaterDepth(bestRoamPosition)) {
if (brain.Navigator.SetDestination(bestRoamPosition, vendor.IsPanicked ? BaseNavigator.NavigationSpeed.Fast : vendor.Config.PreferredSpeed, 0f, 0f)) {
status = StateStatus.Running;
return;
}
}
status = StateStatus.Error;
}
public override void StateLeave(BaseAIBrain brain, BaseEntity entity) {
base.StateLeave(brain, entity);
Stop();
}
public override StateStatus StateThink(float delta, BaseAIBrain brain, BaseEntity entity) {
base.StateThink(delta, brain, entity);
if (Vector3.Distance(brain.Navigator.Destination, (entity as RoamingVendor).Transform.position) < 2f) StateEnter(brain, entity);
if (status == StateStatus.Error) return status;
if (brain.Navigator.Moving) return StateStatus.Running;
return StateStatus.Finished;
}
private void Stop() => brain.Navigator.Stop();
private bool IsPositionATopologyPreference(Vector3 position) {
if (TerrainMeta.TopologyMap != null) {
int topology = TerrainMeta.TopologyMap.GetTopology(position);
if (((int)PREFERRED_TOPOLOGY & topology) > 0 && ((int)BLOCKED_TOPOLOGY & topology) == 0) return true;
}
return false;
}
}
public class InteractionState : BasicAIState {
public InteractionState() : base(AIState.TakeCover) {
}
public override void StateEnter(BaseAIBrain brain, BaseEntity entity) {
base.StateEnter(brain, entity);
RoamingVendor vendor = entity as RoamingVendor;
if (vendor.IsPanicked) {
brain.SwitchToState(AIState.Roam, 1);
return;
}
BasePlayer player = brain.Events.Memory.Entity.Get(brain.Events.CurrentInputMemorySlot) as BasePlayer;
if (player != null) {
vendor.SignalBroadcast(BaseEntity.Signal.Gesture, "wave", null);
vendor.SetAimDirection(Vector3Ex.Direction2D(player.eyes.position, vendor.eyes.position));
brain.Navigator.SetFacingDirectionEntity(player);
}
}
public override void StateLeave(BaseAIBrain brain, BaseEntity entity) {
base.StateLeave(brain, entity);
brain.Navigator.ClearFacingDirectionOverride();
}
public override StateStatus StateThink(float delta, BaseAIBrain brain, BaseEntity entity) {
base.StateThink(delta, brain, entity);
return StateStatus.Running;
}
}
private static readonly byte[] DefaultDesign = new byte[] {
8, 1, 8, 2, 8, 18, 18, 61, 8, 0, 16, 1, 26, 25, 8, 0, 16, 1, 24, 0, 32, 0, 40, 0, 48, 0, 162, 6, 10, 13, 0, 0, 0, 0, 21, 0, 0, 0, 0, 26, 12, 8, 3, 16, 2, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 14, 16, 2, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 62, 8, 1, 16, 2, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 3, 16, 2, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 14, 16, 2, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 48, 8, 2, 16, 18, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 5, 16, 1, 24, 1, 32, 0, 40, 0, 48, 0, 32, 0, 24, 0, 40, 0, 48, 0
}
;
}
#endregion
#region Vending Spawning
private static PaginatedVendingMachine _vendingReference;
private static PaginatedVendingMachine VendingReference {
get {
if (_vendingReference == null) {
const string INVIS_VENDING_MACHINE = "assets/prefabs/deployable/vendingmachine/npcvendingmachines/shopkeeper_vm_invis.prefab";
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab(INVIS_VENDING_MACHINE), Vector3.zero, Quaternion.identity);
gameObject.name = INVIS_VENDING_MACHINE;
gameObject.SetActive(false);
InvisibleVendingMachine invisibleVendingMachine = gameObject.GetComponent<InvisibleVendingMachine>();
PaginatedVendingMachine paginatedVendingMachine = gameObject.AddComponent<PaginatedVendingMachine>();
CopySerializeableFields<InvisibleVendingMachine>(invisibleVendingMachine, paginatedVendingMachine);
paginatedVendingMachine.enableSaving = false;
UnityEngine.Object.DestroyImmediate(invisibleVendingMachine);
_vendingReference = paginatedVendingMachine;
}
return _vendingReference;
}
}
private static PaginatedVendingMachine InstantiateVendingMachine(Vector3 position) {
PaginatedVendingMachine paginatedVendingMachine = UnityEngine.Object.Instantiate<PaginatedVendingMachine>(VendingReference, position, Quaternion.identity);
paginatedVendingMachine.name = VendorReference.name;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(paginatedVendingMachine.gameObject, Rust.Server.EntityScene);
paginatedVendingMachine.gameObject.SetActive(true);
return paginatedVendingMachine;
}
#endregion
#region Vending Components
public class PaginatedVendingMachine : InvisibleVendingMachine {
private int currentPage = 0;
private float[] refillTimes = new float[0];
private bool isInitialized = false;
private readonly List<ConfigData.Vendor.VendingEntry> items = new List<ConfigData.Vendor.VendingEntry>();
private readonly List<ConfigData.Vendor.VendingEntry> validItems = new List<ConfigData.Vendor.VendingEntry>();
public RoamingVendor Vendor {
get;
set;
}
private int TotalPages {
get {
return Mathf.CeilToInt((float)items.Count / (float)TOTAL_PER_PAGE);
}
}
public static readonly Hash<ulong, PaginatedVendingMachine> ActiveVendingPlayers = new Hash<ulong, PaginatedVendingMachine>();
private const int TOTAL_PER_PAGE = 7;
public override void ServerInit() {
vendingOrders = ScriptableObject.CreateInstance<NPCVendingOrder>();
vmoManifest = ScriptableObject.CreateInstance<NPCVendingOrderManifest>();
for (int i = 0; i < Vendor.Config.Items.Count; i++) {
NPCVendingOrder.Entry entry = Vendor.Config.Items[i];
if (entry != null) validItems.Add(Vendor.Config.Items[i]);
}
int maxItems = Mathf.Clamp(Vendor.Config.MaxItems, 0, validItems.Count);
if (maxItems > 0 && maxItems < validItems.Count) {
RandomlySelectSellOrders();
Invoke(RandomlySelectSellOrders, Mathf.Max(Vendor.Config.RefreshRate, 60f));
}
else {
items.AddRange(validItems);
UpdateSellOrderManifest();
}
isInitialized = true;
base.ServerInit();
CancelInvoke(Refill);
InvokeRandomized(DoRefill, 1f, 1f, 0.1f);
shopName = Vendor.Config.VendorName;
}
private void RandomlySelectSellOrders() {
if (ActiveVendingPlayers.Values.Contains(this)) {
Invoke(RandomlySelectSellOrders, 20f);
return;
}
items.Clear();
List<ConfigData.Vendor.VendingEntry> itemList = Pool.GetList<ConfigData.Vendor.VendingEntry>();
itemList.AddRange(validItems);
int maxItems = Mathf.Clamp(Vendor.Config.MaxItems, 0, validItems.Count);
while(items.Count < maxItems) {
if (itemList.Count == 0) break;
ConfigData.Vendor.VendingEntry entry = itemList.GetRandom();
itemList.Remove(entry);
items.Add(entry);
}
Pool.FreeList(ref itemList);
UpdateSellOrderManifest();
InstallFromVendingOrders();
Invoke(RandomlySelectSellOrders, Mathf.Max(Vendor.Config.RefreshRate, 60f));
}
private void UpdateSellOrderManifest() {
if (refillTimes.Length != items.Count) {
Array.Resize(ref refillTimes, items.Count);
for (int i = 0; i < refillTimes.Length; i++) refillTimes[i] = 0;
}
inventorySlots = Mathf.Max(inventorySlots, items.Count);
if (inventory != null) inventory.capacity = inventorySlots;
vendingOrders.orders = items.Select(x => (NPCVendingOrder.Entry)x).ToArray();
vmoManifest.orderList = new NPCVendingOrder[] {
vendingOrders
}
;
}
public override void InstallFromVendingOrders() {
if (!isInitialized) return;
if (items == null || items.Count == 0) {
Debug.LogError("[Roaming NPC Vendors] No vending orders installed!");
return;
}
ClearSellOrders();
inventory.Clear();
ItemManager.DoRemoves();
for (int i = 0; i < items.Count; i++) {
NPCVendingOrder.Entry entry = items[i];
if (entry != null) AddItemForSale(entry.sellItem.itemid, entry.sellItemAmount, entry.currencyItem.itemid, entry.currencyAmount, GetBPState(entry.sellItemAsBP, entry.currencyAsBP), items[i].SellItemSkin, items[i].StockAmount, i < TOTAL_PER_PAGE);
}
}
public void AddItemForSale(int itemID, int amountToSell, int currencyID, int currencyPerTransaction, byte bpState, ulong skinID, int stockAmount, bool addSellOrder) {
if (addSellOrder) AddSellOrder(itemID, amountToSell, currencyID, currencyPerTransaction, bpState);
transactionActive = true;
if (bpState == 1 || bpState == 3) {
for (int i = 0; i < stockAmount; i++) {
Item item = ItemManager.CreateByItemID(blueprintBaseDef.itemid, 1, 0);
item.blueprintTarget = itemID;
inventory.Insert(item);
}
}
else {
inventory.AddItem(ItemManager.FindItemDefinition(itemID), amountToSell * stockAmount, skinID);
}
transactionActive = false;
RefreshSellOrderStockLevel(null);
}
public void ChangePage(BasePlayer player, int direction) {
currentPage = Mathf.Clamp(currentPage + direction, 0, TotalPages);
int startIndex = currentPage * TOTAL_PER_PAGE;
ClearSellOrders();
for (int i = startIndex; i < Mathf.Min(startIndex + TOTAL_PER_PAGE, items.Count);
i++) {
NPCVendingOrder.Entry entry = items[i];
AddSellOrder(entry.sellItem.itemid, entry.sellItemAmount, entry.currencyItem.itemid, entry.currencyAmount, GetBPState(entry.sellItemAsBP, entry.currencyAsBP));
}
ClientRPCPlayer<ProtoBuf.VendingMachine.SellOrderContainer>(null, player, "CLIENT_ReceiveSellOrders", sellOrders);
SetFlag(BaseEntity.Flags.Reserved2, true, false, true);
Invoke(() => SetFlag(BaseEntity.Flags.Reserved2, false, false, true), 0.5f);
CreateOverlay(player);
}
public override bool PlayerOpenLoot(BasePlayer player, string panelToOpen = "", bool doPositionChecks = true) {
if (base.PlayerOpenLoot(player, panelToOpen, doPositionChecks)) {
ActiveVendingPlayers[player.userID] = this;
if (TotalPages > 1) CreateOverlay(player);
return true;
}
return false;
}
public override void PlayerStoppedLooting(BasePlayer player) {
currentPage = 0;
ActiveVendingPlayers.Remove(player.userID);
CuiHelper.DestroyUi(player, UI_PANEL);
base.PlayerStoppedLooting(player);
}
private void DoRefill() {
if (vendingOrders == null || vendingOrders.orders == null) return;
if (inventory == null) return;
for (int i = 0; i < items.Count; i++) {
ConfigData.Vendor.VendingEntry entry = items[i];
if (Time.realtimeSinceStartup > refillTimes[i]) {
int amountPerSaleInInventory = Mathf.FloorToInt((float)(inventory.GetAmount(entry.SellItem.itemid, false) / entry.SellItemAmount));
int refillStockAmount = Mathf.Min(entry.StockAmount - amountPerSaleInInventory, entry.RefillAmount) * entry.SellItemAmount;
//if (entry.SellItemShortname == "radiationresisttea.advanced")
//{
// Debug.Log($"Refill stock ticked for {entry.SellItemShortname}");
// Debug.Log($"Current stock amount {inventory.GetAmount(entry.SellItem.itemid, false)}");
// Debug.Log($"Amount to refill (config : {entry.RefillAmount}) (calculated : {refillStockAmount})");
//}
if (refillStockAmount > 0) {
transactionActive = true;
Item item = null;
if (!entry.SellItemBP) {
item = ItemManager.Create(entry.SellItem, refillStockAmount, entry.SellItemSkin);
}
else {
item = ItemManager.Create(blueprintBaseDef, refillStockAmount);
item.blueprintTarget = entry.SellItem.itemid;
}
if (!item.MoveToContainer(inventory, -1, true, false)) item.Remove(0f);
transactionActive = false;
}
refillTimes[i] = Time.realtimeSinceStartup + entry.RefillDelay;
}
}
}
#region Vending UI
public const string UI_PANEL = "RoamingNPCVendors_UI";
private const string PAGE_COLOR = "0.65 0.65 0.65 0.06";
private const string PAGE_TEXT_COLOR = "0.7 0.7 0.7 1.0";
private const string BUTTON_COLOR = "0.75 0.75 0.75 0.2";
private const string BUTTON_TEXT_COLOR = "0.77 0.68 0.68 1";
private readonly UI4 Container = new UI4(0.9505f, 0.15f, 0.99f, 0.6f);
private readonly UI4 BackButton = new UI4(0f, 0.7f, 1f, 1f);
private readonly UI4 Text = new UI4(0f, 0.3f, 1f, 0.7f);
private readonly UI4 NextButton = new UI4(0f, 0f, 1f, 0.3f);
protected void CreateOverlay(BasePlayer player) {
CuiElementContainer container = UI.Container(UI_PANEL, Container);
UI.Button(container, UI_PANEL, BUTTON_COLOR, "◀", BUTTON_TEXT_COLOR, 50, BackButton, currentPage > 0 ? "roamingnpcvendors.pageprev" : "");
UI.Panel(container, UI_PANEL, PAGE_COLOR, Text);
UI.Label(container, UI_PANEL, $"{currentPage + 1}\nof\n{TotalPages}", PAGE_TEXT_COLOR, 20, Text);
UI.Button(container, UI_PANEL, BUTTON_COLOR, "▶", BUTTON_TEXT_COLOR, 50, NextButton, TotalPages > 1 && currentPage + 1 < TotalPages ? "roamingnpcvendors.pagenext" : "");
CuiHelper.DestroyUi(player, UI_PANEL);
CuiHelper.AddUi(player, container);
}
#endregion
}
#endregion
//private void OnBuyVendingItem(VendingMachine v, BasePlayer p, int sellOrderId, int numberOfTransactions)
//{
// Debug.Log($"{p.displayName} purchased {v.sellOrders.sellOrders[sellOrderId].itemToSellAmount} x {ItemManager.FindItemDefinition(v.sellOrders.sellOrders[sellOrderId].itemToSellID).shortname}");
//}
#region UI
public static class UI {
public static CuiElementContainer Container(string panel, UI4 dimensions, string parent = "Hud.Menu") {
CuiElementContainer container = new CuiElementContainer() {
{
new CuiPanel {
Image = {
Color = "0 0 0 0"
}
, RectTransform = {
AnchorMin = dimensions.GetMin(), AnchorMax = dimensions.GetMax(),
}
}
, new CuiElement().Parent = parent, panel
}
}
;
return container;
}
public static void Panel(CuiElementContainer container, string panel, string color, UI4 dimensions) {
container.Add(new CuiPanel {
Image = {
Color = color
}
, RectTransform = {
AnchorMin = dimensions.GetMin(), AnchorMax = dimensions.GetMax()
}
,
}
, panel);
}
public static void Label(CuiElementContainer container, string panel, string text, string color, int size, UI4 dimensions, TextAnchor align = TextAnchor.MiddleCenter) {
container.Add(new CuiLabel {
Text = {
// ... [Code preview truncated due to length. Full source in attached file] ...