// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.InterestManagement; import engine.Enum; import engine.Enum.DispatchChannel; import engine.Enum.GameObjectType; import engine.gameManager.GroupManager; import engine.gameManager.SessionManager; import engine.job.JobScheduler; import engine.jobs.RefreshGroupJob; import engine.net.AbstractNetMsg; import engine.net.Dispatch; import engine.net.DispatchMessage; import engine.net.client.ClientConnection; import engine.net.client.msg.LoadCharacterMsg; import engine.net.client.msg.LoadStructureMsg; import engine.net.client.msg.MoveToPointMsg; import engine.net.client.msg.UnloadObjectsMsg; import engine.objects.*; import engine.server.MBServerStatics; import org.pmw.tinylog.Logger; import java.util.ArrayList; import java.util.HashSet; import static engine.math.FastMath.sqr; public enum InterestManager implements Runnable { INTERESTMANAGER; private static long lastTime; InterestManager() { Logger.info(" Interest Management thread is running."); } public static void forceLoad(AbstractWorldObject awo) { AbstractNetMsg msg = null; LoadStructureMsg lsm; LoadCharacterMsg lcm; NPC npc; Corpse corpse; HashSet toUpdate; switch (awo.getObjectType()) { case Building: lsm = new LoadStructureMsg(); lsm.addObject((Building) awo); msg = lsm; break; case Corpse: corpse = (Corpse) awo; lcm = new LoadCharacterMsg(corpse, false); msg = lcm; break; case NPC: npc = (NPC) awo; lcm = new LoadCharacterMsg(npc, false); msg = lcm; break; default: return; } toUpdate = WorldGrid.getObjectsInRangePartial(awo.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE, MBServerStatics.MASK_PLAYER); boolean send; for (AbstractWorldObject tar : toUpdate) { PlayerCharacter player = (PlayerCharacter) tar; HashSet loadedStaticObjects = player.getLoadedStaticObjects(); send = false; if (!loadedStaticObjects.contains(awo)) { loadedStaticObjects.add(awo); send = true; } if (send) { Dispatch dispatch = Dispatch.borrow(player, msg); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } } } public static void HandleSpecialUnload(Building building, ClientConnection origin) { if (Regions.FurnitureRegionMap.get(building.getObjectUUID()) == null) return; Regions buildingRegion = Regions.FurnitureRegionMap.get(building.getObjectUUID()); if (!buildingRegion.isOutside()) return; MoveToPointMsg moveMsg = new MoveToPointMsg(building); if (origin != null) origin.sendMsg(moveMsg); } public static void reloadCharacter(AbstractCharacter absChar) { UnloadObjectsMsg uom = new UnloadObjectsMsg(); uom.addObject(absChar); LoadCharacterMsg lcm = new LoadCharacterMsg(absChar, false); HashSet toSend = WorldGrid.getObjectsInRangePartial(absChar.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE, MBServerStatics.MASK_PLAYER); PlayerCharacter pc = null; if (absChar.getObjectType().equals(GameObjectType.PlayerCharacter)) pc = (PlayerCharacter) absChar; for (AbstractWorldObject awo : toSend) { PlayerCharacter pcc = (PlayerCharacter) awo; if (pcc == null) continue; ClientConnection cc = SessionManager.getClientConnection(pcc); if (cc == null) continue; if (pcc.getObjectUUID() == absChar.getObjectUUID()) continue; else { if (pc != null) if (pcc.getSeeInvis() < pc.getHidden()) continue; if (!cc.sendMsg(uom)) { String classType = uom.getClass().getSimpleName(); Logger.error("Failed to send message "); } if (!cc.sendMsg(lcm)) { String classType = lcm.getClass().getSimpleName(); Logger.error("Failed to send message"); } } } } @Override public void run() { beginLoadJob(); } private void beginLoadJob() { InterestManager.lastTime = System.currentTimeMillis(); while (true) { try { updateAllPlayers(); } catch (Exception e) { Logger.error("InterestManager.BeginLoadJob:updateAllPlayers", e); } try { Thread.sleep(advanceOneSecond()); } catch (Exception e) { Logger.error("InterestManager.BeginLoadJob:advanceOneSecond", e); } } } private long advanceOneSecond() { long curTime = System.currentTimeMillis(); long dur = 1000 + this.lastTime - curTime; if (dur < 0) { // Last update took more then one second, not good... Logger.warn("LoadJob took more then one second to complete."); this.lastTime = curTime + 100; return 100; } this.lastTime += 1000; return dur; } // Forces the loading of static objects (corpses and buildings). // Needed to override threshold limits on loading statics private void updateAllPlayers() { // get all players for (PlayerCharacter pc : SessionManager.getAllActivePlayerCharacters()) { if (pc == null) continue; ClientConnection origin = pc.getClientConnection(); if (origin == null) continue; if (!pc.isEnteredWorld()) continue; if (pc.getTeleportLock().readLock().tryLock()) { try { updateStaticList(pc, origin); updateMobileList(pc, origin); } catch (Exception e) { Logger.error(e); } finally { pc.getTeleportLock().readLock().unlock(); } } } } private void updateStaticList(PlayerCharacter player, ClientConnection origin) { // Only update if we've moved far enough to warrant it float distanceSquared = player.getLoc().distanceSquared2D(player.getLastStaticLoc()); if (distanceSquared > sqr(25)) { player.setLastStaticLoc(player.getLoc()); player.dirtyLoad = true; } if (player.dirtyLoad == false) return; // Get Statics in range HashSet toLoad = WorldGrid.getObjectsInRangePartial(player.getLoc(), MBServerStatics.STRUCTURE_LOAD_RANGE, MBServerStatics.MASK_STATIC); // get list of obects loaded that need removed HashSet loadedStaticObjects = player.getLoadedStaticObjects(); HashSet toRemove = null; toRemove = new HashSet<>(loadedStaticObjects); toRemove.removeAll(toLoad); // unload static objects now out of range if (toRemove.size() > 0) { UnloadObjectsMsg uom = new UnloadObjectsMsg(); for (AbstractWorldObject obj : toRemove) { if (obj.getObjectType().equals(GameObjectType.Building)) InterestManager.HandleSpecialUnload((Building) obj, origin); if (obj != null && !obj.equals(player)) uom.addObject(obj); } Dispatch dispatch = Dispatch.borrow(player, uom); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } loadedStaticObjects.removeAll(toRemove); // remove any object to load that are already loaded toLoad.removeAll(loadedStaticObjects); LoadStructureMsg lsm = new LoadStructureMsg(); LoadCharacterMsg lcm = null; ArrayList lcmList = new ArrayList<>(); for (AbstractWorldObject awo : toLoad) { if (awo.getObjectType().equals(GameObjectType.Building)) lsm.addObject((Building) awo); else if (awo.getObjectType().equals(GameObjectType.Corpse)) { Corpse corpse = (Corpse) awo; lcm = new LoadCharacterMsg(corpse, PlayerCharacter.hideNonAscii()); Dispatch dispatch = Dispatch.borrow(player, lcm); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } else if (awo.getObjectType().equals(GameObjectType.NPC)) { NPC npc = (NPC) awo; lcm = new LoadCharacterMsg(npc, PlayerCharacter.hideNonAscii()); lcmList.add(lcm); } } if (lsm.getStructureList().size() > 0) { Dispatch dispatch = Dispatch.borrow(player, lsm); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } for (LoadCharacterMsg lc : lcmList) { Dispatch dispatch = Dispatch.borrow(player, lc); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } loadedStaticObjects.addAll(toLoad); player.dirtyLoad = false; } private void updateMobileList(PlayerCharacter player, ClientConnection origin) { if (player == null) return; // Get list of players in range // TODO for now use a generic getALL list, later tie into Quad Tree HashSet toLoad = WorldGrid.getObjectsInRangePartial(player.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE, MBServerStatics.MASK_MOBILE); HashSet toRemove = new HashSet<>(); HashSet toLoadToPlayer = new HashSet<>(); for (AbstractWorldObject loadedObject : toLoad) { switch (loadedObject.getObjectType()) { case PlayerCharacter: PlayerCharacter loadedPlayer = (PlayerCharacter) loadedObject; if (loadedPlayer.getObjectUUID() == player.getObjectUUID()) continue; if (player.getSeeInvis() < loadedPlayer.getHidden()) continue; if (loadedPlayer.safemodeInvis()) continue; if (player.getLoadedObjects().contains(loadedPlayer)) continue; if (!loadedPlayer.isInWorldGrid()) continue; toLoadToPlayer.add(loadedPlayer); break; //not playerCharacter, mobs,npcs and corpses cant be invis or safemode, just add normaly default: if (player.getLoadedObjects().contains(loadedObject)) continue; if (!loadedObject.isInWorldGrid()) continue; toLoadToPlayer.add(loadedObject); break; } } float unloadDistance = MBServerStatics.CHARACTER_LOAD_RANGE; for (AbstractWorldObject playerLoadedObject : player.getLoadedObjects()) { if (playerLoadedObject.getObjectType().equals(GameObjectType.PlayerCharacter)) { PlayerCharacter loadedPlayer = (PlayerCharacter) playerLoadedObject; if (player.getSeeInvis() < loadedPlayer.getHidden()) toRemove.add(playerLoadedObject); else if (loadedPlayer.safemodeInvis()) toRemove.add(playerLoadedObject); } if (!playerLoadedObject.isInWorldGrid()) toRemove.add(playerLoadedObject); else if (playerLoadedObject.getLoc().distanceSquared2D(player.getLoc()) > unloadDistance * unloadDistance) toRemove.add(playerLoadedObject); } player.getLoadedObjects().addAll(toLoadToPlayer); player.getLoadedObjects().removeAll(toRemove); // get list of obects loaded to remove // unload objects now out of range if (toRemove.size() > 0) { UnloadObjectsMsg uom = new UnloadObjectsMsg(); for (AbstractWorldObject obj : toRemove) { try { if (obj != null) if (obj.equals(player)) // don't unload self continue; uom.addObject(obj); if (obj.getObjectType() == GameObjectType.Mob) ((Mob) obj).playerAgroMap.remove(player.getObjectUUID()); } catch (Exception e) { Logger.error("UnloadCharacter", obj.getObjectUUID() + " " + e.getMessage()); } } if (!uom.getObjectList().isEmpty()) { Dispatch dispatch = Dispatch.borrow(player, uom); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } } LoadCharacterMsg lcm = null; ArrayList players = new ArrayList<>(); ArrayList addToList = new ArrayList<>(); for (AbstractWorldObject awo : toLoadToPlayer) { try { if (awo.equals(player)) // dont load yourself continue; if ((awo.getObjectTypeMask() & MBServerStatics.MASK_PLAYER) != 0) { // object to load is a player PlayerCharacter awopc = (PlayerCharacter) awo; // dont load if invis if (player.getSeeInvis() < awopc.getHidden()) continue; lcm = new LoadCharacterMsg(awopc, PlayerCharacter.hideNonAscii()); players.add(awo); // check if in a group with the person being loaded // and if so set updateGroup flag if (GroupManager.getGroup(player) != null && GroupManager.getGroup(player) == GroupManager.getGroup(awopc)) // submit a job as for some reason the client needs a delay // with group updates // as it wont update if we do RefreshGroup directly after // sending the lcm below JobScheduler.getInstance().scheduleJob(new RefreshGroupJob(player, awopc), MBServerStatics.LOAD_OBJECT_DELAY); } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_MOB) != 0) { Mob awonpc = (Mob) awo; if (!awonpc.isAlive() && (awonpc.isPet() || awonpc.isSiege() || awonpc.isNecroPet() || awonpc.isPlayerGuard())) continue; //removed, interest manager should still load mob corpses if (awonpc.despawned == true) continue; awonpc.playerAgroMap.put(player.getObjectUUID(), false); ((Mob) awonpc).setCombatTarget(null); lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii()); } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_NPC) != 0) { NPC awonpc = (NPC) awo; lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii()); } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_PET) != 0) { Mob awonpc = (Mob) awo; if (!awonpc.isAlive()) continue; awonpc.playerAgroMap.put(player.getObjectUUID(), false); if ((awonpc.agentType.equals(Enum.AIAgentType.MOBILE))) ((Mob) awonpc).setCombatTarget(null); lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii()); } addToList.add(awo); if (lcm != null) { Dispatch dispatch = Dispatch.borrow(player, lcm); DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); } } catch (Exception e) { Logger.error(awo.getObjectUUID() + " " + e.getMessage()); } //Delaying character loading to reduce bandwidth consumption } // send effects for all players being loaded // do it on a timer otherwise we may get failures as te client needs // time to process lcm //Added effects to LoadCharacter Serialization. //JobScheduler.getInstance().scheduleJob(new LoadEffectsJob(players, origin), MBServerStatics.LOAD_OBJECT_DELAY); } public synchronized void HandleLoadForEnterWorld(PlayerCharacter player) { if (player == null) return; ClientConnection origin = player.getClientConnection(); if (origin == null) return; //Update static list try { updateStaticList(player, origin); } catch (Exception e) { Logger.error("InterestManager.updateAllStaticPlayers: " + player.getObjectUUID(), e); } //Update mobile list try { updateMobileList(player, origin); } catch (Exception e) { Logger.error("InterestManager.updateAllMobilePlayers: " + player.getObjectUUID(), e); } } public synchronized void HandleLoadForTeleport(PlayerCharacter player) { if (player == null) return; ClientConnection origin = player.getClientConnection(); if (origin == null) return; //Update static list try { updateStaticList(player, origin); } catch (Exception e) { Logger.error("InterestManager.updateAllStaticPlayers: " + player.getObjectUUID(), e); } //Update mobile list try { updateMobileList(player, origin); } catch (Exception e) { Logger.error("InterestManager.updateAllMobilePlayers: " + player.getObjectUUID(), e); } } public static void setObjectDirty(AbstractWorldObject abstractWorldObject) { HashSet playerList = WorldGrid.getObjectsInRangePartial(abstractWorldObject.getLoc(), 128, MBServerStatics.MASK_PLAYER); if (playerList.isEmpty()) return; for (AbstractWorldObject playerObject : playerList) { PlayerCharacter playerCharacter = (PlayerCharacter) playerObject; playerCharacter.dirtyLoad = true; } } }