// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.gameManager; import engine.Enum.DispatchChannel; import engine.Enum.GameObjectType; import engine.Enum.ModType; import engine.Enum.SourceType; import engine.InterestManagement.InterestManager; import engine.exception.MsgSendException; import engine.math.Bounds; import engine.math.Vector3f; import engine.math.Vector3fImmutable; import engine.net.DispatchMessage; import engine.net.client.ClientConnection; import engine.net.client.msg.MoveToPointMsg; import engine.net.client.msg.TeleportToPointMsg; import engine.net.client.msg.UpdateStateMsg; import engine.objects.*; import engine.server.MBServerStatics; import org.pmw.tinylog.Logger; import java.util.Set; import static engine.math.FastMath.sqr; public enum MovementManager { MOVEMENTMANAGER; private static final String changeAltitudeTimerJobName = "ChangeHeight"; private static final String flightTimerJobName = "Flight"; public static void sendOOS(PlayerCharacter pc) { pc.setWalkMode(true); MovementManager.sendRWSSMsg(pc); } public static void sendRWSSMsg(AbstractCharacter ac) { if (!ac.isAlive()) return; UpdateStateMsg rssm = new UpdateStateMsg(); rssm.setPlayer(ac); if (ac.getObjectType() == GameObjectType.PlayerCharacter) DispatchMessage.dispatchMsgToInterestArea(ac, rssm, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); else DispatchMessage.sendToAllInRange(ac, rssm); } /* * Sets the first combat target for the AbstractCharacter. Used to clear the * combat * target upon each move, unless something has set the firstHitCombatTarget * Also used to determine the size of a monster's hitbox */ public static void movement(MoveToPointMsg msg, AbstractCharacter toMove) throws MsgSendException { // check for stun/root if (!toMove.isAlive()) return; if (toMove.getObjectType().equals(GameObjectType.PlayerCharacter)) { if (((PlayerCharacter) toMove).isCasting()) ((PlayerCharacter) toMove).update(); if(((PlayerCharacter) toMove).isFlying() && toMove.getEffects().containsKey("MoveBuff")) PlayerCharacter.GroundPlayer(((PlayerCharacter) toMove)); } toMove.setIsCasting(false); toMove.setItemCasting(false); if (toMove.getBonuses().getBool(ModType.Stunned, SourceType.None) || toMove.getBonuses().getBool(ModType.CannotMove, SourceType.None)) { return; } if (msg.getEndLat() > MBServerStatics.MAX_WORLD_WIDTH) msg.setEndLat((float) MBServerStatics.MAX_WORLD_WIDTH); if (msg.getEndLon() < MBServerStatics.MAX_WORLD_HEIGHT) { msg.setEndLon((float) MBServerStatics.MAX_WORLD_HEIGHT); } // if (msg.getEndLat() < 0) // msg.setEndLat(0); // // if (msg.getEndLon() > 0) // msg.setEndLon(0); if (!toMove.isMoving()) toMove.resetLastSetLocUpdate(); else toMove.update(); // Update movement for the player // else if (toMove.getObjectType() == GameObjectType.Mob) // ((Mob)toMove).updateLocation(); // get start and end locations for the move Vector3fImmutable startLocation = new Vector3fImmutable(msg.getStartLat(), msg.getStartAlt(), msg.getStartLon()); Vector3fImmutable endLocation = new Vector3fImmutable(msg.getEndLat(), msg.getEndAlt(), msg.getEndLon()); // if (toMove.getObjectType() == GameObjectType.PlayerCharacter) // if (msg.getEndAlt() == 0 && msg.getTargetID() == 0){ // MovementManager.sendRWSSMsg(toMove); // } //If in Building, let's see if we need to Fix // if inside a building, convert both locations from the building local reference frame to the world reference frame if (msg.getTargetID() > 0) { Building building = BuildingManager.getBuildingFromCache(msg.getTargetID()); if (building != null) { Vector3fImmutable convertLocEnd = new Vector3fImmutable(ZoneManager.convertLocalToWorld(building, endLocation)); // if (!Bounds.collide(convertLocEnd, b) || !b.loadObjectsInside()) { // toMove.setInBuilding(-1); // toMove.setInFloorID(-1); // toMove.setInBuildingID(0); // } // else { toMove.setInBuilding(msg.getInBuilding()); toMove.setInFloorID(msg.getUnknown01()); toMove.setInBuildingID(msg.getTargetID()); msg.setStartCoord(ZoneManager.convertWorldToLocal(building, toMove.getLoc())); if (toMove.getObjectType() == GameObjectType.PlayerCharacter) { if (convertLocEnd.distanceSquared2D(toMove.getLoc()) > 6000 * 6000) { Logger.info("ENDLOC:" + convertLocEnd.x + ',' + convertLocEnd.y + ',' + convertLocEnd.z + ',' + "GETLOC:" + toMove.getLoc().x + ',' + toMove.getLoc().y + ',' + toMove.getLoc().z + " Name " + ((PlayerCharacter) toMove).getCombinedName()); toMove.teleport(toMove.getLoc()); return; } } startLocation = toMove.getLoc(); endLocation = convertLocEnd; } else { toMove.setInBuilding(-1); toMove.setInFloorID(-1); toMove.setInBuildingID(0); //SYNC PLAYER toMove.teleport(toMove.getLoc()); return; } } else { toMove.setInBuildingID(0); toMove.setInFloorID(-1); toMove.setInBuilding(-1); msg.setStartCoord(toMove.getLoc()); } //make sure we set the correct player. msg.setSourceType(toMove.getObjectType().ordinal()); msg.setSourceID(toMove.getObjectUUID()); //if player in region, modify location to local location of building. set target to building. if (toMove.region != null) { Building regionBuilding = Regions.GetBuildingForRegion(toMove.region); if (regionBuilding != null) { msg.setStartCoord(ZoneManager.convertWorldToLocal(Regions.GetBuildingForRegion(toMove.region), toMove.getLoc())); msg.setEndCoord(ZoneManager.convertWorldToLocal(regionBuilding, endLocation)); msg.setInBuilding(toMove.region.level); msg.setUnknown01(toMove.region.room); msg.setTargetType(GameObjectType.Building.ordinal()); msg.setTargetID(regionBuilding.getObjectUUID()); } } else { toMove.setInBuildingID(0); toMove.setInFloorID(-1); toMove.setInBuilding(-1); msg.setStartCoord(toMove.getLoc()); msg.setEndCoord(endLocation); msg.setTargetType(0); msg.setTargetID(0); } //checks sync between character and server, if out of sync, teleport player to original position and return. if (toMove.getObjectType() == GameObjectType.PlayerCharacter) { boolean startLocInSync = checkSync(toMove, startLocation, toMove.getLoc()); if (!startLocInSync) { syncLoc(toMove, toMove.getLoc(), startLocInSync); return; } } // set direction, based on the current location which has just been sync'd // with the client and the calc'd destination toMove.setFaceDir(endLocation.subtract2D(toMove.getLoc()).normalize()); boolean collide = false; if (toMove.getObjectType().equals(GameObjectType.PlayerCharacter)) { Vector3fImmutable collidePoint = Bounds.PlayerBuildingCollisionPoint((PlayerCharacter) toMove, toMove.getLoc(), endLocation); if (collidePoint != null) { msg.setEndCoord(collidePoint); endLocation = collidePoint; collide = true; } } if (toMove.getObjectType() == GameObjectType.PlayerCharacter && ((PlayerCharacter) toMove).isTeleportMode()) { toMove.teleport(endLocation); return; } // move to end location, this can interrupt the current move toMove.setEndLoc(endLocation); // ChatManager.chatSystemInfo((PlayerCharacter)toMove, "Moving to " + Vector3fImmutable.toString(endLocation)); // make sure server knows player is not sitting toMove.setSit(false); // cancel any effects that break upon movement toMove.cancelOnMove(); //cancel any attacks for manual move. if ((toMove.getObjectType() == GameObjectType.PlayerCharacter) && msg.getUnknown02() == 0) toMove.setCombatTarget(null); // If it's not a player moving just send the message if ((toMove.getObjectType() == GameObjectType.PlayerCharacter) == false) { DispatchMessage.sendToAllInRange(toMove, msg); return; } // If it's a player who is moving then we need to handle characters // who should see the message via group follow PlayerCharacter player = (PlayerCharacter) toMove; player.setTimeStamp("lastMoveGate", System.currentTimeMillis()); if (collide) DispatchMessage.dispatchMsgToInterestArea(player, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); else DispatchMessage.dispatchMsgToInterestArea(player, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false); // Handle formation movement if needed if (player.getFollow() == false) return; City cityObject = null; Zone serverZone = null; serverZone = ZoneManager.findSmallestZone(player.getLoc()); cityObject = (City) DbManager.getFromCache(GameObjectType.City, serverZone.getPlayerCityUUID()); // Do not send group messages if player is on grid if (cityObject != null) return; // If player is not in a group we can exit here Group group = GroupManager.getGroup(player); if (group == null) return; // Echo group movement messages if (group.getGroupLead().getObjectUUID() == player.getObjectUUID()) moveGroup(player, player.getClientConnection(), msg); } /** * compare client and server location to verify that the two are in sync * * @param ac the player character * @param clientLoc location as reported by the client * @param serverLoc location known to the server * @return true if the two are in sync */ private static boolean checkSync(AbstractCharacter ac, Vector3fImmutable clientLoc, Vector3fImmutable serverLoc) { float desyncDist = clientLoc.distanceSquared2D(serverLoc); // desync logging if (MBServerStatics.MOVEMENT_SYNC_DEBUG) if (desyncDist > MBServerStatics.MOVEMENT_DESYNC_TOLERANCE * MBServerStatics.MOVEMENT_DESYNC_TOLERANCE) // our current location server side is a calc of last known loc + direction + speed and known time of last update Logger.debug("Movement out of sync for " + ac.getFirstName() + ", Server Loc: " + serverLoc.getX() + ' ' + serverLoc.getZ() + " , Client loc: " + clientLoc.getX() + ' ' + clientLoc.getZ() + " desync distance " + desyncDist + " moving=" + ac.isMoving()); else Logger.debug("Movement sync is good - desyncDist = " + desyncDist); if (ac.getDebug(1) && ac.getObjectType().equals(GameObjectType.PlayerCharacter)) if (desyncDist > MBServerStatics.MOVEMENT_DESYNC_TOLERANCE * MBServerStatics.MOVEMENT_DESYNC_TOLERANCE) { PlayerCharacter pc = (PlayerCharacter) ac; ChatManager.chatSystemInfo(pc, "Movement out of sync for " + ac.getFirstName() + ", Server Loc: " + serverLoc.getX() + ' ' + serverLoc.getZ() + " , Client loc: " + clientLoc.getX() + ' ' + clientLoc.getZ() + " desync distance " + desyncDist + " moving=" + ac.isMoving()); } // return indicator that the two are in sync or not return (desyncDist < 100f * 100f); } public static void finishChangeAltitude(AbstractCharacter ac, float targetAlt) { if (ac.getObjectType().equals(GameObjectType.PlayerCharacter) == false) return; //reset the getLoc timer before we clear other timers // otherwise the next call to getLoc will not be correct ac.resetLastSetLocUpdate(); // call getLoc once as it processes loc to the ms Vector3fImmutable curLoc = ac.getLoc(); if (MBServerStatics.MOVEMENT_SYNC_DEBUG) Logger.info("Finished Alt change, setting the end location to " + ac.getEndLoc().getX() + ' ' + ac.getEndLoc().getZ() + " moving=" + ac.isMoving() + " and current location is " + curLoc.getX() + ' ' + curLoc.getZ()); if (ac.getDebug(1) && ac.getObjectType().equals(GameObjectType.PlayerCharacter)) ChatManager.chatSystemInfo((PlayerCharacter) ac, "Finished Alt change, setting the end location to " + ac.getEndLoc().getX() + ' ' + ac.getEndLoc().getZ() + " moving=" + ac.isMoving() + " and current location is " + curLoc.getX() + ' ' + curLoc.getZ()); //Send run/walk/sit/stand to tell the client we are flying / landing etc ac.update(); ac.stopMovement(ac.getLoc()); if (ac.isAlive()) MovementManager.sendRWSSMsg(ac); //Check collision again } // Handle formation movement in group public static void moveGroup(PlayerCharacter pc, ClientConnection origin, MoveToPointMsg msg) throws MsgSendException { // get forward vector Vector3f faceDir = new Vector3f(pc.getFaceDir().x, 0, pc.getFaceDir().z).normalize(); // get perpendicular vector Vector3f crossDir = new Vector3f(faceDir.z, 0, -faceDir.x); //get source loc with altitude Vector3f sLoc = new Vector3f(pc.getLoc().x, pc.getAltitude(), pc.getLoc().z); Group group = GroupManager.getGroup(pc); Set members = group.getMembers(); int pos = 0; for (PlayerCharacter member : members) { if (member == null) continue; if (member.getObjectUUID() == pc.getObjectUUID()) continue; MoveToPointMsg groupMsg = new MoveToPointMsg(msg); // Verify group member should be moved pos++; if (member.getFollow() != true) continue; //get member loc with altitude, then range against source loc Vector3f mLoc = new Vector3f(member.getLoc().x, member.getAltitude(), member.getLoc().z); if (sLoc.distanceSquared2D(mLoc) > sqr(MBServerStatics.FORMATION_RANGE)) continue; //don't move if player has taken damage from another player in last 60 seconds long lastAttacked = System.currentTimeMillis() - pc.getLastPlayerAttackTime(); if (lastAttacked < 60000) continue; if (!member.isAlive()) continue; //don't move if player is stunned or rooted PlayerBonuses bonus = member.getBonuses(); if (bonus.getBool(ModType.Stunned, SourceType.None) || bonus.getBool(ModType.CannotMove, SourceType.None)) continue; member.update(); // All checks passed, let's move the player // First get the offset position Vector3f offset = Formation.getOffset(group.getFormation(), pos); Vector3fImmutable destination = pc.getEndLoc(); // offset forwards or backwards destination = destination.add(faceDir.mult(offset.z)); // offset left or right destination = destination.add(crossDir.mult(offset.x)); // ArrayList awoList = WorldGrid.INSTANCE.getObjectsInRangePartial(member, member.getLoc().distance2D(destination) +1000, MBServerStatics.MASK_BUILDING); // // boolean skip = false; // // for (AbstractWorldObject awo: awoList){ // Building building = (Building)awo; // // if (building.getBounds() != null){ // if (Bounds.collide(building, member.getLoc(), destination)){ // skip = true; // break; // } // // } // // } // // if (skip) // continue; // if (member.isMoving()) // member.stopMovement(); // Update player speed to match group lead speed and make standing if (member.isSit() || (member.isWalk() != pc.isWalk())) { member.setSit(false); member.setWalkMode(pc.isWalk()); MovementManager.sendRWSSMsg(member); } //cancel any effects that break upon movement member.cancelOnMove(); // send movement for other players to see groupMsg.setSourceID(member.getObjectUUID()); groupMsg.setStartCoord(member.getLoc()); groupMsg.setEndCoord(destination); groupMsg.clearTarget(); DispatchMessage.sendToAllInRange(member, groupMsg); // update group member member.setFaceDir(destination.subtract2D(member.getLoc()).normalize()); member.setEndLoc(destination); } } public static void translocate(AbstractCharacter teleporter, Vector3fImmutable targetLoc, Regions region) { if (targetLoc == null) return; Vector3fImmutable oldLoc = new Vector3fImmutable(teleporter.getLoc()); teleporter.stopMovement(targetLoc); teleporter.setRegion(region); //mobs ignore region sets for now. if (teleporter.getObjectType().equals(GameObjectType.Mob)) { teleporter.setInBuildingID(0); teleporter.setInBuilding(-1); teleporter.setInFloorID(-1); TeleportToPointMsg msg = new TeleportToPointMsg(teleporter, targetLoc.getX(), targetLoc.getY(), targetLoc.getZ(), 0, -1, -1); DispatchMessage.dispatchMsgToInterestArea(oldLoc, teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false); return; } TeleportToPointMsg msg = new TeleportToPointMsg(teleporter, targetLoc.getX(), targetLoc.getY(), targetLoc.getZ(), 0, -1, -1); //we shouldnt need to send teleport message to new area, as loadjob should pick it up. // DispatchMessage.dispatchMsgToInterestArea(teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); DispatchMessage.dispatchMsgToInterestArea(oldLoc, teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); if (teleporter.getObjectType().equals(GameObjectType.PlayerCharacter)) InterestManager.INTERESTMANAGER.HandleLoadForTeleport((PlayerCharacter) teleporter); } private static void syncLoc(AbstractCharacter ac, Vector3fImmutable clientLoc, boolean useClientLoc) { ac.teleport(ac.getLoc()); } }