using System; using System.Collections.Generic; using System.Reflection; using System.Text.Json; using ModernUO.Serialization; using Server.Commands; using Server.Items; using Server.Json; using Server.Mobiles; using static Server.Attributes; namespace Server.Engines.Spawners; [SerializationGenerator(10, false)] public abstract partial class BaseSpawner : Item, ISpawner { [SerializedIgnoreDupe] [SerializableField(0)] [SerializedCommandProperty(AccessLevel.Developer)] private Guid _guid; [SerializableField(1)] [SerializedCommandProperty(AccessLevel.Developer)] private bool _returnOnDeactivate; [SerializedIgnoreDupe] [SerializableField(2, setter: "private")] private List _entries; [InvalidateProperties] [SerializableField(3)] [SerializedCommandProperty(AccessLevel.Developer)] private int _walkingRange = -1; [SerializableField(4)] [SerializedCommandProperty(AccessLevel.Developer)] private WayPoint _wayPoint; [InvalidateProperties] [SerializableField(5)] [SerializedCommandProperty(AccessLevel.Developer)] private bool _group; [InvalidateProperties] [SerializableField(6)] [SerializedCommandProperty(AccessLevel.Developer)] private TimeSpan _minDelay; [InvalidateProperties] [SerializableField(7)] [SerializedCommandProperty(AccessLevel.Developer)] private TimeSpan _maxDelay; [InvalidateProperties] [SerializableField(9)] [SerializedCommandProperty(AccessLevel.Developer)] private int _team; [InvalidateProperties] [SerializableField(10)] [SerializedCommandProperty(AccessLevel.Developer)] private int _homeRange; [SerializableField(12)] [SerializedCommandProperty(AccessLevel.Developer)] private DateTime _end; private InternalTimer _timer; public BaseSpawner() : this(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, 4) { } public BaseSpawner(string spawnedName) : this( 1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, 4, spawnedName ) { } public BaseSpawner( int amount, int minDelay, int maxDelay, int team, int homeRange, params string[] spawnedNames ) : this( amount, TimeSpan.FromMinutes(minDelay), TimeSpan.FromMinutes(maxDelay), team, homeRange, spawnedNames ) { } public BaseSpawner( int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, params string[] spawnedNames ) : base(0x1f13) { _guid = Guid.NewGuid(); InitSpawn(amount, minDelay, maxDelay, team, homeRange); for (var i = 0; i < spawnedNames.Length; i++) { AddEntry(spawnedNames[i], 100, amount, false); } } public BaseSpawner(DynamicJson json, JsonSerializerOptions options) : base(0x1f13) { if (!json.GetProperty("guid", options, out _guid)) { _guid = Guid.NewGuid(); } if (json.GetProperty("name", options, out string name)) { Name = name; } json.GetProperty("count", options, out int amount); json.GetProperty("minDelay", options, out TimeSpan minDelay); json.GetProperty("maxDelay", options, out TimeSpan maxDelay); json.GetProperty("team", options, out int team); json.GetProperty("homeRange", options, out int homeRange); json.GetProperty("walkingRange", options, out int walkingRange); _walkingRange = walkingRange; InitSpawn(amount, minDelay, maxDelay, team, homeRange); json.GetProperty("entries", options, out List entries); foreach (var entry in entries) { AddEntry(entry.SpawnedName, entry.SpawnedProbability, entry.SpawnedMaxCount, false, entry.Properties, entry.Parameters); } } public override string DefaultName => "Spawner"; public bool IsFull => Spawned?.Count >= _count; public bool IsEmpty => Spawned?.Count == 0; [IgnoreDupe] public Dictionary Spawned { get; private set; } [SerializableProperty(8)] [CommandProperty(AccessLevel.Developer)] public int Count { get => _count; set { _count = value; if (IsFull) { _timer?.Stop(); } else if (_timer?.Running != true) { DoTimer(); } InvalidateProperties(); this.MarkDirty(); } } [SerializableProperty(11)] [CommandProperty(AccessLevel.Developer)] public bool Running { get => _running; set { if (value) { Start(); } else { Stop(); } InvalidateProperties(); this.MarkDirty(); } } [CommandProperty(AccessLevel.Developer)] public TimeSpan NextSpawn { get => _running && _timer?.Running == true ? End - Core.Now : TimeSpan.Zero; set { if (!_running && Entries.Count > 0) { _running = true; DoTimer(value); } } } public virtual Point3D HomeLocation => Location; public bool UnlinkOnTaming => true; Region ISpawner.Region => Region.Find(Location, Map); public void UpdateEntries(List entries) { Entries = entries; } public void Remove(ISpawnable spawn) { Defrag(); if (spawn != null) { Spawned.Remove(spawn, out var entry); entry?.RemoveFromSpawned(spawn); } if (_running && !IsFull && _timer?.Running != true) { DoTimer(); } } public virtual void Respawn() { RemoveSpawns(); for (var i = 0; i < _count; i++) { Spawn(); } DoTimer(); // Turn off the timer! } public virtual void Reset() { Stop(); RemoveSpawns(); } public virtual void ToJson(DynamicJson json, JsonSerializerOptions options) { json.Type = GetType().Name; json.SetProperty("name", options, Name); json.SetProperty("guid", options, _guid); json.SetProperty("location", options, Location); json.SetProperty("map", options, Map); json.SetProperty("count", options, Count); json.SetProperty("minDelay", options, MinDelay); json.SetProperty("maxDelay", options, MaxDelay); json.SetProperty("team", options, Team); json.SetProperty("homeRange", options, HomeRange); json.SetProperty("walkingRange", options, WalkingRange); json.SetProperty("entries", options, Entries); } public abstract Point3D GetSpawnPosition(ISpawnable spawned, Map map); public override void OnAfterDuped(Item newItem) { if (newItem is BaseSpawner newSpawner) { newSpawner._guid = Guid.NewGuid(); newSpawner.Spawned = new Dictionary(); newSpawner.Entries = []; for (var i = 0; i < Entries.Count; i++) { var entry = Entries[i]; newSpawner.AddEntry( entry.SpawnedName, entry.SpawnedProbability, entry.SpawnedMaxCount, false, entry.Properties, entry.Parameters ); } } } public SpawnerEntry AddEntry( string creaturename, int probability = 100, int amount = 1, bool dotimer = true, string properties = null, string parameters = null ) { var entry = new SpawnerEntry(this, creaturename, probability, amount, properties, parameters); AddToEntries(entry); if (dotimer) { DoTimer(TimeSpan.FromSeconds(1)); } return entry; } public void InitSpawn(int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange) { Visible = false; Movable = false; _running = true; _group = false; _minDelay = minDelay; _maxDelay = maxDelay; _count = amount; _team = team; _homeRange = homeRange; Entries = []; Spawned = new Dictionary(); DoTimer(TimeSpan.FromSeconds(1)); } public override bool CanSeeStaffOnly(Mobile from) => from.AccessLevel >= AccessLevel.Developer; public override bool IsAccessibleTo(Mobile from) => from.AccessLevel >= AccessLevel.Developer; public override void OnDoubleClick(Mobile from) { from.SendGump(new SpawnerGump(this)); } public virtual void GetSpawnerProperties(IPropertyList list) { } public override void GetProperties(IPropertyList list) { base.GetProperties(list); if (_running) { list.Add(1060742); // active list.Add(1060656, _count); // amount to make: ~1_val~ list.Add(1061169, _homeRange); // range ~1_val~ list.Add(1050039, $"{"walking range:"}\t{_walkingRange}"); // ~1_NUMBER~ ~2_ITEMNAME~ list.Add(1053099, $"{"group:"}\t{_group}"); // ~1_oretype~: ~2_armortype~ list.Add(1060847, $"{"team:"}\t{_team}"); // ~1_val~ ~2_val~ list.Add(1063483, $"{"delay:"}\t{_minDelay} to {_maxDelay}"); // ~1_MATERIAL~: ~2_ITEMNAME~ GetSpawnerProperties(list); for (var i = 0; i < 6 && i < Entries.Count; ++i) { var entry = Entries[i]; list.Add(1060658 + i, $"\t{entry.SpawnedName}\t{CountSpawns(entry)}"); } } else { list.Add(1060743); // inactive } } public override void OnSingleClick(Mobile from) { base.OnSingleClick(from); if (_running) { LabelTo(from, "[Running]"); } else { LabelTo(from, "[Off]"); } } public void Start() { if (!_running && Entries.Count > 0) { _running = true; DoTimer(); } } public void Stop() { _timer?.Stop(); _running = false; } public void Defrag() { Entries ??= []; for (var i = 0; i < Entries.Count; ++i) { Entries[i].Defrag(this); } } public virtual bool OnDefragSpawn(ISpawnable spawned, bool remove) { if (!remove) // Override could have set it to true already { remove = spawned.Deleted || spawned.Spawner == null || spawned switch { Item item => item.RootParent is Mobile || item.IsLockedDown || item.IsSecure, Mobile m => m is BaseCreature c && (c.Controlled || c.IsStabled), _ => true }; } return remove && Spawned.Remove(spawned); } public void OnTick() { if (_group) { Defrag(); if (Spawned.Count > 0) { return; } Respawn(); } else { Spawn(); } DoTimer(); } public virtual void Spawn() { Defrag(); if (Entries.Count <= 0 || IsFull) { return; } var probsum = 0; foreach (var spawnerEntry in Entries) { if (!spawnerEntry.IsFull) { probsum += spawnerEntry.SpawnedProbability; } } if (probsum <= 0) { return; } var rand = Utility.RandomMinMax(1, probsum); for (var i = 0; i < Entries.Count; i++) { var entry = Entries[i]; if (entry.IsFull) { continue; } if (rand <= entry.SpawnedProbability) { Spawn(entry, out var flags); entry.Valid = flags; return; } rand -= entry.SpawnedProbability; } } private static string[,] FormatProperties(string[] args) { string[,] props; var remains = args.Length; if (remains >= 2) { props = new string[remains / 2, 2]; remains /= 2; for (var j = 0; j < remains; ++j) { props[j, 0] = args[j * 2]; props[j, 1] = args[j * 2 + 1]; } } else { props = new string[0, 0]; } return props; } private static PropertyInfo[] GetTypeProperties(Type type, string[,] props) { PropertyInfo[] realProps = null; if (props != null) { realProps = new PropertyInfo[props.GetLength(0)]; var allProps = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); for (var i = 0; i < realProps.Length; ++i) { PropertyInfo thisProp = null; var propName = props[i, 0]; for (var j = 0; thisProp == null && j < allProps.Length; ++j) { if (propName.InsensitiveEquals(allProps[j].Name)) { thisProp = allProps[j]; } } if (thisProp == null) { return null; } var attr = GetCPA(thisProp); if (attr == null || attr.WriteLevel > AccessLevel.Developer || !thisProp.CanWrite || attr.ReadOnly) { return null; } realProps[i] = thisProp; } } return realProps; } public bool Spawn(int index, out EntryFlags flags) { if (index >= 0 && index < Entries.Count) { return Spawn(Entries[index], out flags); } flags = EntryFlags.InvalidEntry; return false; } public bool Spawn(SpawnerEntry entry, out EntryFlags flags) { var map = GetSpawnMap(); flags = EntryFlags.None; if (map == null || map == Map.Internal || Parent != null) { return false; } // Defrag taken care of in Spawn(), beforehand // Count check taken care of in Spawn(), beforehand var type = AssemblyHandler.FindTypeByName(entry.SpawnedName); if (type == null) { flags = EntryFlags.InvalidType; return false; } try { IEntity entity = null; var propargs = string.IsNullOrEmpty(entry.Properties) ? [] : CommandSystem.Split(entry.Properties.Trim()); var props = FormatProperties(propargs); var realProps = GetTypeProperties(type, props); if (realProps == null) { flags = EntryFlags.InvalidProps; return false; } var paramargs = string.IsNullOrEmpty(entry.Parameters) ? [] : entry.Parameters.Trim().Split(' '); if (paramargs.Length == 0) { entity = type.CreateInstance( ci => IsConstructible(ci, AccessLevel.Developer) ); } else { var ctors = type.GetConstructors(); for (var i = 0; i < ctors.Length; ++i) { var ctor = ctors[i]; if (IsConstructible(ctor, AccessLevel.Developer)) { var paramList = ctor.GetParameters(); if (paramargs.Length == paramList.Length) { var paramValues = Add.ParseValues(paramList, paramargs); if (paramValues != null) { entity = ctor.Invoke(paramValues) as IEntity; break; } } } } } if (entity == null) { flags = EntryFlags.InvalidType | EntryFlags.InvalidParams; return false; } for (var i = 0; i < realProps.Length; i++) { if (realProps[i] != null) { var result = Types.TryParse( realProps[i].PropertyType, props[i, 1], out var toSet ); if (result == null) { realProps[i].SetValue(entity, toSet, null); } else { flags = EntryFlags.InvalidProps; (entity as ISpawnable)?.Delete(); return false; } } } if (entity is Mobile m) { Spawned.Add(m, entry); entry.AddToSpawned(m); var loc = m is BaseVendor ? Location : GetSpawnPosition(m, map); m.OnBeforeSpawn(loc, map); m.MoveToWorld(loc, map); if (m is BaseCreature c) { var walkrange = GetWalkingRange(); c.RangeHome = walkrange >= 0 ? walkrange : _homeRange; c.CurrentWayPoint = WayPoint; if (_team > 0) { c.Team = _team; } c.Home = Location; c.HomeMap = Map; } m.Spawner = this; m.OnAfterSpawn(); } else if (entity is Item item) { Spawned.Add(item, entry); entry.AddToSpawned(item); var loc = GetSpawnPosition(item, map); item.OnBeforeSpawn(loc, map); item.MoveToWorld(loc, map); item.Spawner = this; item.OnAfterSpawn(); } else { // Other IEntity types that might get created are simply not supported flags = EntryFlags.InvalidType | EntryFlags.InvalidParams; return false; } } catch (Exception e) { Console.WriteLine($"EXCEPTION CAUGHT: {Serial}"); Console.WriteLine(e); return false; } ClearProperties(); return true; } public virtual int GetWalkingRange() => _walkingRange; public virtual Map GetSpawnMap() => Map; public void DoTimer() { if (!_running) { return; } var min = (long)_minDelay.TotalMilliseconds; var max = (long)_maxDelay.TotalMilliseconds; var delay = TimeSpan.FromMilliseconds(Utility.RandomMinMax(min, max)); DoTimer(delay); } public virtual void DoTimer(TimeSpan delay) { if (!_running) { return; } if (delay <= TimeSpan.Zero) { End = Core.Now; } else { End = Core.Now + delay; } if (_timer == null) { _timer = new InternalTimer(this, delay); } else { _timer.Stop(); _timer.Delay = delay; } if (!IsFull) { _timer.Start(); } } public int CountSpawns(SpawnerEntry entry) { Defrag(); return entry.Spawned.Count; } public void RemoveEntry(SpawnerEntry entry) { Defrag(); for (var i = entry.Spawned.Count - 1; i >= 0; i--) { var e = entry.Spawned[i]; entry.Spawned.RemoveAt(i); e?.Delete(); } Entries.Remove(entry); if (_running && !IsFull && _timer?.Running != true) { DoTimer(); } InvalidateProperties(); } public void RemoveSpawn(int index) // Entry { if (index >= 0 && index < Entries.Count) { RemoveSpawn(Entries[index]); } } public void RemoveSpawn(SpawnerEntry entry) { for (var i = entry.Spawned.Count - 1; i >= 0; i--) { var e = entry.Spawned[i]; if (e != null) { entry.Spawned.RemoveAt(i); Spawned.Remove(e); e.Delete(); } } } public void RemoveSpawns() { Defrag(); for (var i = 0; i < Entries.Count; i++) { var entry = Entries[i]; for (var j = entry.Spawned.Count - 1; j >= 0; j--) { var e = entry.Spawned[j]; if (e != null) { Spawned.Remove(e); entry.Spawned.RemoveAt(j); e.Delete(); } } } if (_running && !IsFull && _timer?.Running != true) { DoTimer(); } InvalidateProperties(); } public void BringToHome() { Defrag(); foreach (var e in Spawned.Keys) { e?.MoveToWorld(Location, Map); } } public override void OnDelete() { base.OnDelete(); Stop(); RemoveSpawns(); } private void Deserialize(IGenericReader reader, int version) { _guid = reader.ReadGuid(); _returnOnDeactivate = reader.ReadBool(); var count = reader.ReadInt(); _entries = new List(count); for (var i = 0; i < count; ++i) { var entry = new SpawnerEntry(this); entry.Deserialize(reader); _entries.Add(entry); } _walkingRange = reader.ReadInt(); _wayPoint = reader.ReadEntity(); _group = reader.ReadBool(); _minDelay = reader.ReadTimeSpan(); _maxDelay = reader.ReadTimeSpan(); _count = reader.ReadInt(); _team = reader.ReadInt(); _homeRange = reader.ReadInt(); _running = reader.ReadBool(); _end = _running ? reader.ReadDeltaTime() : Core.Now; } [AfterDeserialization] private void AfterDeserialization() { Spawned = new Dictionary(); foreach (var entry in Entries) { foreach (var spawned in entry.Spawned) { Spawned.Add(spawned, entry); } } DoTimer(_end - Core.Now); } private class InternalTimer : Timer { private readonly BaseSpawner _spawner; public InternalTimer(BaseSpawner spawner, TimeSpan delay) : base(delay) => _spawner = spawner; protected override void OnTick() { if (_spawner?.Deleted == false) { _spawner.OnTick(); } } } } [Flags] public enum EntryFlags { None = 0x000, InvalidType = 0x001, InvalidParams = 0x002, InvalidProps = 0x004, InvalidEntry = 0x008 }