using System; using System.Collections.Generic; using System.Linq; using ACE.Common; using ACE.Entity.Enum; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Managers; namespace ACE.Server.WorldObjects { /// /// A generator handles spawning other WorldObjects /// partial class WorldObject { /// /// The generator that spawned this WorldObject /// public WorldObject Generator { get; set; } /// /// Returns TRUE if this object is a generator /// (spawns other world objects) /// public bool IsGenerator { get => GeneratorProfiles != null && GeneratorProfiles.Count > 0; } //public List History = new List(); /// /// A generator can have multiple profiles / spawn multiple types of objects /// Each generator profile can in turn spawn multiple objects (init_create / max_create) /// public List GeneratorProfiles; /// /// Creates a list of active generator profiles /// from a list of biota generators /// public void AddGeneratorProfiles() { GeneratorProfiles = new List(); uint i = 0; if (Biota.PropertiesGenerator != null) { foreach (var generator in Biota.PropertiesGenerator) GeneratorProfiles.Add(new GeneratorProfile(this, generator, i++)); } } /// /// Initialize Generator system /// public void InitializeGenerator() { // ensure if Max <= 0 (or defaulted to 0 from null) is not less than Init. // Profiles may have different settings but the core slots require Max to be greater than 0 if Init is greater than 0 // defaulting to Max == Init for our purposes. if ((MaxGeneratedObjects <= 0 || MaxGeneratedObjects < InitGeneratedObjects) && InitGeneratedObjects > 0) { log.Warn($"[GENERATOR] 0x{Guid.Full} {Name}.InitializeGenerator: {WeenieClassName} ({WeenieClassId}) MaxGeneratedObjects = {MaxGeneratedObjects} | InitGeneratedObjects = {InitGeneratedObjects}. Setting MaxGeneratedObjects = InitGeneratedObjects"); MaxGeneratedObjects = InitGeneratedObjects; } AddGeneratorProfiles(); } /// /// The number of currently spawned objects + /// the number of objects currently in the spawn queue /// public int CurrentCreate { get => GeneratorProfiles.Select(i => i.CurrentCreate).Sum(); } /// /// A list of indices into GeneratorProfiles where CurrentCreate > 0 or is on cooldown /// public List GeneratorActiveProfiles { get { var activeProfiles = new List(); for (var i = 0; i < GeneratorProfiles.Count; i++) { var profile = GeneratorProfiles[i]; if (profile.CurrentCreate > 0 || !profile.IsAvailable) activeProfiles.Add(i); } return activeProfiles; } } /// /// Returns TRUE if all generator profiles are at max objects created /// public bool AllProfilesMaxed => !GeneratorProfiles.Any(i => !i.IsPlaceholder && !i.IsMaxed); /// /// Returns TRUE if all generator profiles are unavailable /// public bool AllProfilesUnavailable => !GeneratorProfiles.Any(i => !i.IsPlaceholder && i.IsAvailable); /// /// Adds object(s) to the spawn queue from a single RNG roll /// public void SelectAProfile() { //History.Add($"[{DateTime.UtcNow}] - SelectAProfile()"); //bool rng_selected = false; if (GenStopSelectProfileConditions) return; //var totalProbability = rng_selected ? GetTotalProbability() : 1.0f; //var rng = ThreadSafeRandom.Next(0.0f, totalProbability); //var rng = ThreadSafeRandom.Next(0.0f, 1.0f); var rng = ThreadSafeRandom.Next(0.0f, GetTotalProbability()); for (var i = 0; i < GeneratorProfiles.Count; i++) { var profile = GeneratorProfiles[i]; // skip PlaceHolder objects if (profile.IsPlaceholder) continue; // is this profile already at its max_create? if (profile.IsMaxed) continue; // is this profile currently timed out? if (!profile.IsAvailable) continue; if (profile.RegenLocationType.HasFlag(RegenLocationType.Treasure)) { if (profile.Biota.InitCreate > 1) { log.Warn($"[GENERATOR] 0x{Guid} {Name}.SelectAProfile(): profile[{i}].RegenLocationType({profile.RegenLocationType}), profile.Biota.WCID({profile.Biota.WeenieClassId}), profile.Biota.InitCreate({profile.Biota.InitCreate}) > 1, set to 1. WCID: {WeenieClassId} - LOC: {Location.ToLOCString()}"); profile.Biota.InitCreate = 1; } if (profile.Biota.MaxCreate > 1) { log.Warn($"[GENERATOR] 0x{Guid} {Name}.SelectAProfile(): profile[{i}].RegenLocationType({profile.RegenLocationType}), profile.Biota.WCID({profile.Biota.WeenieClassId}), profile.Biota.MaxCreate({profile.Biota.MaxCreate}) > 1, set to 1. WCID: {WeenieClassId} - LOC: {Location.ToLOCString()}"); profile.Biota.MaxCreate = 1; } } //var probability = rng_selected ? GetAdjustedProbability(i) : profile.Biota.Probability; //var probability = profile.Biota.Probability; var probability = GetAdjustedProbability(i); if (rng < probability || probability == -1) { var numObjects = GetSpawnObjectsForProfile(profile); profile.Enqueue(numObjects); //log.Info($"[GENERATOR] 0x{Guid} {Name}.SelectAProfile(): profile[{i}] Enqueued {numObjects} {profile.Biota.WeenieClassId} for spawning. MaxObjectsSpawned = {profile.MaxObjectsSpawned} | Exhusted = {profile.RemoveQueue.Count == profile.MaxCreate} | {profile.CurrentCreate} | {profile.MaxCreate} | {profile.Spawned.Count} | {profile.RemoveQueue.Count}"); //var rng_str = probability == -1 ? "" : "RNG "; //History.Add($"[{DateTime.UtcNow}] - SelectAProfile() - {rng_str}selected slot {i} to spawn, adding {numObjects} objects ({profile.CurrentCreate}/{profile.MaxCreate})"); // if RNG rolled, we are done with this roll if (profile.Biota.Probability != -1) { //rng_selected = true; break; } // stop conditions if (GenStopSelectProfileConditions) return; } } } /// /// Returns the total probability of all RNG profiles /// which arent at max objects spawned yet or on cooldown /// public float GetTotalProbability() { var totalProbability = 0.0f; var lastProbability = 0.0f; foreach (var profile in GeneratorProfiles) { var probability = profile.Biota.Probability; if (probability == -1) { //if (!profile.IsMaxed) if (!profile.IsMaxed && profile.IsAvailable) return 1.0f; continue; } //if (!profile.IsMaxed) if (!profile.IsMaxed && profile.IsAvailable) { if (lastProbability > probability) lastProbability = 0.0f; var diff = probability - lastProbability; totalProbability += diff; } lastProbability = probability; } return totalProbability; } /// /// Returns the max probability from all the generator profiles /// public float GetMaxProbability() { var maxProbability = float.MinValue; // note: this will also include maxed generator profiles! foreach (var profile in GeneratorProfiles) { var probability = profile.Biota.Probability; if (probability > maxProbability) maxProbability = probability; } return maxProbability; } /// /// Returns the adjust probability for a generator profile index, /// taking into account previous profile probabilities which are already at max objects spawned or on cooldown /// public float GetAdjustedProbability(int index) { // say theres a generator with 2 profiles // the first has a 99% chance to spawn, and the second has a 1% chance // the generator init_create is 1, and the max_create is 2 // when the generator first spawns in, the first object is created, as expected // then the hearbeat happens later, and it sees it can spawn up to 1 additional object // the rare item is the only item left, with the 1 % chance // so the question is, in that scenario, would the rare item always spawn then? // or would it only do 1 roll, and the rare item would still have only a 1% chance to spawn? var profile = GeneratorProfiles[index]; if (profile.Biota.Probability == -1) return -1; var totalProbability = 0.0f; var lastProbability = 0.0f; for (var i = 0; i <= index; i++) { profile = GeneratorProfiles[i]; var probability = profile.Biota.Probability; if (probability == -1) continue; //if (!profile.IsMaxed) if (!profile.IsMaxed && profile.IsAvailable) { if (lastProbability > probability) lastProbability = 0.0f; var diff = probability - lastProbability; totalProbability += diff; } lastProbability = probability; } return totalProbability; } /// /// Get the current number of objects to spawn for a specific profile /// public int GetSpawnObjectsForProfile(GeneratorProfile profile) { // get the number of objects to spawn for this profile // usually profile.InitCreate, must be at least profile.InitCreate while not to exceed generator.MaxCreate and profile.MaxCreate, // -1 for profile.InitCreate == 1 // -1 for profile.MaxCreate == profile can be spawned infinitely as long as generator.MaxCreate has not been met. var initCreate = profile.InitCreate; var maxCreate = profile.MaxCreate; var numObjects = 0; if (initCreate == -1 || maxCreate == -1) numObjects = 1; else numObjects = initCreate; //Console.WriteLine($"INIT - 0x{Guid} {Name} ({WeenieClassId}): CurrentCreate = {CurrentCreate} | profile.Biota.InitCreate = {profile.Biota.InitCreate} | profile.Biota.MaxCreate = {profile.Biota.MaxCreate} | InitCreate: {InitCreate} | MaxCreate: {MaxCreate} | initCreate: {initCreate} | maxCreate: {maxCreate} | leftObjects = {leftObjects} | numObjects: {numObjects}"); var genSlotsAvailable = MaxCreate - CurrentCreate; var profileSlotsAvailable = profile.MaxCreate - profile.CurrentCreate; if (genSlotsAvailable < numObjects) numObjects = genSlotsAvailable; if (profile.MaxCreate != -1 && profileSlotsAvailable < numObjects) numObjects = profileSlotsAvailable; if (numObjects == 0 && initCreate == 0) log.Warn($"[GENERATOR] 0x{Guid}:{WeenieClassId} {Name}.GetSpawnObjectsForProfile(profile[{profile.LinkId}]): profile.InitCreate = {profile.InitCreate} | profile.MaxCreate = {profile.MaxCreate} | profile.WeenieClassId = {profile.WeenieClassId} | Profile Init invalid, cannot spawn."); else if (numObjects == 0) log.Warn($"[GENERATOR] 0x{Guid}:{WeenieClassId} {Name}.GetSpawnObjectsForProfile(profile[{profile.LinkId}]): profile.InitCreate = {profile.InitCreate} | profile.MaxCreate = {profile.MaxCreate} | profile.WeenieClassId = {profile.WeenieClassId} | genSlotsAvailable = {genSlotsAvailable} | profileSlotsAvailable = {profileSlotsAvailable} | numObjects = {numObjects}, cannot spawn."); return numObjects; } /// /// Returns TRUE if stop conditions have been reached for aborting generator profile selection /// public bool GenStopSelectProfileConditions { get { if (CurrentCreate >= MaxCreate) { //if (CurrentCreate > InitCreate) //log.DebugFormat("{0} - 0x{1}:{2}.StopConditionsInit(): CurrentCreate({3}) > InitCreate({4})", WeenieClassId, Guid, Name, CurrentCreate, InitCreate); return true; } if (CurrentlyPoweringUp && CurrentCreate >= InitCreate) { return true; } return AllProfilesUnavailable || AllProfilesMaxed; } } /// /// Enables/disables a generator based on time status /// public void CheckGeneratorStatus() { switch (GeneratorTimeType) { // TODO: defined case GeneratorTimeType.RealTime: CheckRealTimeStatus(); break; case GeneratorTimeType.Event: CheckEventStatus(); break; case GeneratorTimeType.Night: case GeneratorTimeType.Day: CheckTimeOfDayStatus(); break; } } /// /// Enables/disables a generator based on in-game time of day, Day or Night /// public void CheckTimeOfDayStatus() { var prevDisabled = GeneratorDisabled; var isDay = Timers.CurrentInGameTime.IsDay; var isDayGenerator = GeneratorTimeType == GeneratorTimeType.Day; //GeneratorDisabled = isDay != isDayGenerator; //HandleStatus(prevDisabled); HandleStatusStaged(prevDisabled, isDay, isDayGenerator); } /// /// Enables/disables a generator based on realtime between start/end time /// public void CheckRealTimeStatus() { var prevDisabled = GeneratorDisabled; var now = (int)Time.GetUnixTime(); var start = (now < GeneratorStartTime) && (GeneratorStartTime > 0); var end = (now > GeneratorEndTime) && (GeneratorEndTime > 0); //GeneratorDisabled = ((now < GeneratorStartTime) && (GeneratorStartTime > 0)) || ((now > GeneratorEndTime) && (GeneratorEndTime > 0)); //HandleStatus(prevDisabled); HandleStatusStaged(prevDisabled, start, end); } /// /// Enables/disables a generator based on world event status /// public void CheckEventStatus() { if (string.IsNullOrEmpty(GeneratorEvent)) return; var prevState = GeneratorDisabled; if (!EventManager.IsEventAvailable(GeneratorEvent)) return; var enabled = EventManager.IsEventEnabled(GeneratorEvent); var started = EventManager.IsEventStarted(GeneratorEvent, this, null); //GeneratorDisabled = !enabled || !started; //HandleStatus(prevState); HandleStatusStaged(prevState, enabled, started); } /// /// Handles starting/stopping the generator /// public void HandleStatus(bool prevDisabled) { if (prevDisabled == GeneratorDisabled) return; // no state change if (prevDisabled) StartGenerator(); else DisableGenerator(); } private bool eventStatusChanged = false; /// /// Handles starting/stopping the generator /// public void HandleStatusStaged(bool prevDisabled, bool cond1, bool cond2) { var change = false; switch (GeneratorTimeType) { case GeneratorTimeType.RealTime: change = cond1 || cond2; break; case GeneratorTimeType.Event: change = !cond1 || !cond2; break; case GeneratorTimeType.Day: case GeneratorTimeType.Night: change = cond1 != cond2; break; } if (eventStatusChanged) { GeneratorDisabled = change; if (!GeneratorDisabled) StartGenerator(); else DisableGenerator(); eventStatusChanged = false; } else { if (prevDisabled != change) eventStatusChanged = true; } } /// /// Called when a generator is first created /// public void StartGenerator() { //Console.WriteLine($"{Name}.StartGenerator()"); if (CurrentlyPoweringUp) return; CurrentlyPoweringUp = true; if (GeneratorInitialDelay > 0) { NextGeneratorRegenerationTime = GetNextRegenerationTime(GeneratorInitialDelay); CurrentLandblock?.ResortWorldObjectIntoSortedGeneratorRegenerationList(this); } else { if (this is Container && this is not Creature || (!string.IsNullOrEmpty(GeneratorEvent) && RegenerationInterval == 0)) Generator_Generate(); if (InitCreate == 0) CurrentlyPoweringUp = false; } } private double GetNextRegenerationTime(double generatorInitialDelay) { if (RegenerationTimestamp == 0) return Time.GetUnixTime(); return Time.GetUnixTime() + generatorInitialDelay; } /// /// Disables a generator and Processes GeneratorDestructionDirective /// public void DisableGenerator() { // generator has been disabled, potentially destroy, kill or leave behind everything in registry and reset back to defaults ProcessGeneratorDestructionDirective(GeneratorEndDestructionType); } /// /// Called upon death of a generator and Processes GeneratorDestructionDirective /// public void OnGeneratorDeath() { // generator has been killed, potentially destroy, kill or leave behind everything in registry and reset back to defaults ProcessGeneratorDestructionDirective(GeneratorDestructionType); } /// /// Called upon death of a generator and Processes GeneratorDestructionDirective /// public void OnGeneratorDestroy() { // generator has been destroyed, potentially destroy, kill or leave behind everything in registry and reset back to defaults ProcessGeneratorDestructionDirective(GeneratorDestructionType); } /// /// Destroys/Kills all of its spawned objects, if specifically directed, and resets back to default /// public void ProcessGeneratorDestructionDirective(GeneratorDestruct generatorDestructType, bool fromLandblockUnload = false) { switch (generatorDestructType) { case GeneratorDestruct.Kill: foreach (var generator in GeneratorProfiles) { generator.KillAll(); } break; case GeneratorDestruct.Destroy: foreach (var generator in GeneratorProfiles) { generator.DestroyAll(fromLandblockUnload); } break; case GeneratorDestruct.Nothing: default: break; } } /// /// Callback system for objects notifying their generators of events, /// ie. item pickup /// public void NotifyOfEvent(RegenerationType regenerationType) { if (GeneratorId == null || Generator == null) return; var generator = Generator.GeneratorProfiles.FirstOrDefault(g => g.Spawned.ContainsKey(Guid.Full)); generator?.NotifyGenerator(Guid, regenerationType); if (Generator.GeneratorId > 0) // Generator is controlled by another generator. { if ((!(Generator is Container) || Generator.GeneratorAutomaticDestruction) && Generator.InitCreate > 0 && Generator.CurrentCreate == 0) // Parent generator is non-container (Container, Corpse, Chest, Slumlord, Storage, Hook, Creature) generator Generator.Destroy(); // Generator's complete spawn count has been wiped out } else if (Generator.GeneratorAutomaticDestruction && Generator.InitCreate > 0 && Generator.CurrentCreate == 0) { Generator.Destroy(); // Generator's complete spawn count has been wiped out } //if (!Generator.IsDestroyed) // Generator.SelectAProfile(); Generator = null; GeneratorId = null; } /// /// Called by ActivateLinks in WorldObject_Links for generators /// public void AddGeneratorLinks() { if (GeneratorProfiles == null || GeneratorProfiles.Count == 0) { Console.WriteLine($"{Name}.AddGeneratorLinks(): no profiles to link!"); return; } var profileTemplate = GeneratorProfiles[0]; foreach (var link in LinkedInstances) { var profile = new PropertiesGenerator(); profile.WeenieClassId = link.WeenieClassId; profile.ObjCellId = link.ObjCellId; profile.OriginX = link.OriginX; profile.OriginY = link.OriginY; profile.OriginZ = link.OriginZ; profile.AnglesW = link.AnglesW; profile.AnglesX = link.AnglesX; profile.AnglesY = link.AnglesY; profile.AnglesZ = link.AnglesZ; profile.Delay = profileTemplate.Biota.Delay; profile.Probability = profileTemplate.Biota.Probability; profile.InitCreate = profileTemplate.Biota.InitCreate; profile.MaxCreate = profileTemplate.Biota.MaxCreate; profile.WhenCreate = profileTemplate.Biota.WhenCreate; profile.WhereCreate = profileTemplate.Biota.WhereCreate; GeneratorProfiles.Add(new GeneratorProfile(this, profile, link.Guid)); if (profile.Probability == -1) { InitCreate += profile.InitCreate; MaxCreate += profile.MaxCreate; } } } /// /// Called every [RegenerationInterval] seconds /// Also called from EmoteManager, Chest.Reset(), WorldObject.OnGenerate() /// public void Generator_Update() { //Console.WriteLine($"{Name}.Generator_HeartBeat({HeartbeatInterval})"); if (!FirstEnterWorldDone) FirstEnterWorldDone = true; CheckGeneratorStatus(); if (!GeneratorEnteredWorld) { CheckGeneratorStatus(); // due to staging if generator hadn't entered world, reprocess CheckGeneratorStatus for first generator status to update if (!GeneratorDisabled) StartGenerator(); // spawn initial objects for this generator GeneratorEnteredWorld = true; } } /// /// Called every [RegenerationInterval] seconds /// Also called from EmoteManager, Chest.Reset(), WorldObject.OnGenerate() /// public void Generator_Generate() { //Console.WriteLine($"{Name}.Generator_Generate({RegenerationInterval})"); if (!GeneratorDisabled) { if (CurrentlyPoweringUp) { //Console.WriteLine($"{Name}.Generator_Generate({RegenerationInterval}) SelectAProfile: Init={InitCreate} Current={CurrentCreate} Max={MaxCreate} GenStopSelectProfileConditions={GenStopSelectProfileConditions}"); var genLoopCount = 0; while (!GenStopSelectProfileConditions) { SelectAProfile(); genLoopCount++; if (genLoopCount > 1000) { log.Error($"[GENERATOR] 0x{Guid} {Name}.Generator_Generate(): genLoopCount > 1000, aborted init spawn. GenStopSelectProfileConditions: {GenStopSelectProfileConditions} | InitCreate: {InitCreate} | CurrentCreate: {CurrentCreate} | WCID: {WeenieClassId} - LOC: {Location.ToLOCString()}"); break; } } CurrentlyPoweringUp = false; } else { //Console.WriteLine($"{Name}.Generator_Generate({RegenerationInterval}) SelectAProfile: Init={InitCreate} Current={CurrentCreate} Max={MaxCreate}"); SelectAProfile(); } } foreach (var profile in GeneratorProfiles) profile.Spawn_HeartBeat(); } public virtual void ResetGenerator() { foreach (var generator in GeneratorProfiles) { generator.Reset(); } } public uint? GetStaticGuid(uint dynamicGuid) { foreach (var profile in GeneratorProfiles) { if (profile.Spawned.ContainsKey(dynamicGuid)) return profile.Id; } return null; } } }