// ============================================================ // YALootBoxes_Manager.c // Owns the lifecycle of all lootboxes: initial spawn, periodic // despawn/respawn checks, and proximity "requires X" messages. // Lives in: scripts/4_World/YALootBoxes_Manager.c // // Instantiated once from mission init (see init.c snippet at // bottom of this file). // ============================================================ class YALB_ActiveBoxEntry { YALootBoxes_Base box; string locationName; // which spawn_locations.json entry it's using }; class YALootBoxesManager { protected ref array m_ActiveBoxes; protected ref array m_FreeLocationNames; // locations currently unused (the live pool) protected ref map m_PlayerMessageCooldown; // player UID -> last message world time void YALootBoxesManager() { m_ActiveBoxes = new array(); m_FreeLocationNames = new array(); m_PlayerMessageCooldown = new map(); } void Init() { YALB_ConfigLoader.LoadAll(); if (!YALB_ConfigLoader.SpawnLocations || YALB_ConfigLoader.SpawnLocations.SpawnLocations.Count() == 0) { Log_Error("No spawn locations loaded. Manager will not spawn any lootboxes."); return; } foreach (YALB_SpawnLocationDef loc : YALB_ConfigLoader.SpawnLocations.SpawnLocations) { m_FreeLocationNames.Insert(loc.name); } // Remove any lootboxes left over from a previous session (e.g. ones // that persisted) BEFORE spawning, so they don't stack on restart. CleanupOrphanedBoxes(); SpawnInitialBatch(); // Periodic lifecycle tick (despawn/respawn checks) float intervalMs = YALB_ConfigLoader.MainConfig.GlobalCheckIntervalSeconds * 1000; GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(TickLifecycle, intervalMs, true); // Proximity message tick - runs more frequently than the // lifecycle check since it needs to feel responsive to players GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(TickProximityMessages, 1500, true); Log_Info("Manager initialized. " + m_ActiveBoxes.Count() + " lootboxes spawned."); } // Scans every configured spawn location and deletes any YALootBoxes_Base // found there. Catches boxes that persisted from a previous session so a // restart doesn't stack a fresh set on top of the old one. (New boxes are // also spawned non-persistent below, so this is mostly a one-time cleanup // for boxes created before that change.) protected void CleanupOrphanedBoxes() { int removed = 0; array nearby = new array(); array cargos = new array(); foreach (YALB_SpawnLocationDef loc : YALB_ConfigLoader.SpawnLocations.SpawnLocations) { nearby.Clear(); cargos.Clear(); GetGame().GetObjectsAtPosition3D(loc.GetPosition(), 5.0, nearby, cargos); foreach (Object o : nearby) { YALootBoxes_Base box = YALootBoxes_Base.Cast(o); if (box) { GetGame().ObjectDelete(box); removed++; } } } if (removed > 0) Log_Info("Removed " + removed + " leftover lootbox(es) from a previous session before spawning."); } protected void SpawnInitialBatch() { int target = YALB_ConfigLoader.MainConfig.MaxActiveLootboxes; for (int i = 0; i < target; i++) { if (!SpawnOneAtRandomFreeLocation()) break; // ran out of free locations } } // Picks a free location, picks the box type config matching that // location's boxType, spawns the entity, marks location occupied. protected bool SpawnOneAtRandomFreeLocation() { if (m_FreeLocationNames.Count() == 0) return false; int idx = Math.RandomInt(0, m_FreeLocationNames.Count()); string locName = m_FreeLocationNames[idx]; YALB_SpawnLocationDef locDef = FindLocationDef(locName); if (!locDef) { m_FreeLocationNames.Remove(idx); return SpawnOneAtRandomFreeLocation(); // try again with a different one } YALB_BoxTypeDef typeDef = YALB_ConfigLoader.GetBoxTypeByType(locDef.boxType); if (!typeDef) { Log_Warn("Spawn location '" + locName + "' references unknown boxType '" + locDef.boxType + "'. Skipping."); m_FreeLocationNames.Remove(idx); return SpawnOneAtRandomFreeLocation(); } // ECE_NOPERSISTENCY_WORLD: never saved to the world DB, so boxes don't // survive a restart and stack. ECE_SETUP gives full entity init (cargo // etc.); ECE_CREATEPHYSICS gives collision/selection geometry. Object obj = GetGame().CreateObjectEx(typeDef.className, locDef.GetPosition(), ECE_SETUP | ECE_CREATEPHYSICS | ECE_NOPERSISTENCY_WORLD); YALootBoxes_Base box = YALootBoxes_Base.Cast(obj); if (!box) { Log_Error("Failed to spawn class '" + typeDef.className + "' at '" + locName + "'."); m_FreeLocationNames.Remove(idx); return SpawnOneAtRandomFreeLocation(); } box.SetPosition(locDef.GetPosition()); box.Init(locDef.boxType); YALB_ActiveBoxEntry entry = new YALB_ActiveBoxEntry(); entry.box = box; entry.locationName = locName; m_ActiveBoxes.Insert(entry); m_FreeLocationNames.Remove(idx); Log_Info(string.Format("Spawned '%1' at location '%2' (%3).", typeDef.className, locName, locDef.GetPosition().ToString())); return true; } protected YALB_SpawnLocationDef FindLocationDef(string name) { foreach (YALB_SpawnLocationDef d : YALB_ConfigLoader.SpawnLocations.SpawnLocations) { if (d.name == name) return d; } return null; } // Called every GlobalCheckIntervalSeconds. Removes expired boxes // (whether still locked or already opened) and frees their location, // then immediately tries to fill the gap at a NEW random location. void TickLifecycle() { float now = GetGame().GetTime() / 1000.0; array toRemove = new array(); foreach (YALB_ActiveBoxEntry entry : m_ActiveBoxes) { if (!entry.box) { toRemove.Insert(entry); continue; } bool expired = entry.box.HasExpiredUnopened(now) || entry.box.HasExpiredAfterOpening(now); if (expired) { toRemove.Insert(entry); } } foreach (YALB_ActiveBoxEntry deadEntry : toRemove) { RemoveAndFreeLocation(deadEntry); } // Backfill up to MaxActiveLootboxes using newly-freed (and any // previously unused) locations. int deficit = YALB_ConfigLoader.MainConfig.MaxActiveLootboxes - m_ActiveBoxes.Count(); for (int i = 0; i < deficit; i++) { if (!SpawnOneAtRandomFreeLocation()) break; } Log_Debug(string.Format("Lifecycle tick: removed %1, active now %2.", toRemove.Count(), m_ActiveBoxes.Count())); } protected void RemoveAndFreeLocation(YALB_ActiveBoxEntry entry) { m_ActiveBoxes.RemoveItem(entry); m_FreeLocationNames.Insert(entry.locationName); if (entry.box) { GetGame().ObjectDelete(entry.box); } } // Broadcasts "Requires: X, Y" to any player standing within range // of a LOCKED box, regardless of what they're holding (per design: // always show required items, even empty-handed). void TickProximityMessages() { if (!YALB_ConfigLoader.MainConfig) return; float range = YALB_ConfigLoader.MainConfig.ProximityMessageRangeMeters; float now = GetGame().GetTime() / 1000.0; array players = new array(); GetGame().GetWorld().GetPlayerList(players); foreach (Man m : players) { PlayerBase player = PlayerBase.Cast(m); if (!player || !player.GetIdentity()) continue; YALootBoxes_Base nearest = FindNearestLockedBox(player.GetPosition(), range); if (!nearest) continue; // Light cooldown per player so we're not spamming every 1.5s string playerId = player.GetIdentity().GetPlainId(); float lastShown = 0; m_PlayerMessageCooldown.Find(playerId, lastShown); if (now - lastShown < 4.0) continue; YALB_NotifyPlayer(player, nearest.GetRequiredItemsMessage()); m_PlayerMessageCooldown.Set(playerId, now); Log_Verbose("Proximity message sent to '" + player.GetIdentity().GetName() + "' near box type '" + nearest.GetBoxTypeId() + "'."); } } protected YALootBoxes_Base FindNearestLockedBox(vector pos, float maxRange) { YALootBoxes_Base closest = null; float closestDist = maxRange; foreach (YALB_ActiveBoxEntry entry : m_ActiveBoxes) { if (!entry.box || !entry.box.IsLocked()) continue; float dist = vector.Distance(entry.box.GetPosition(), pos); if (dist <= closestDist) { closest = entry.box; closestDist = dist; } } return closest; } }; // ============================================================ // BOOTSTRAP // This manager is started automatically by the modded MissionServer // in scripts/5_Mission/YALootBoxes_MissionServer.c (OnInit). No // init.c edit is required. // // If you instead run a mission that does NOT go through MissionServer // and want to start it manually, the equivalent init.c hook is: // // if (GetGame().IsServer()) // { // g_YALootBoxesManager = new YALootBoxesManager(); // g_YALootBoxesManager.Init(); // } // ============================================================