forked from MagicBane/Server
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
505 lines
17 KiB
505 lines
17 KiB
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . |
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· |
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ |
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ |
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ |
|
// 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(); |
|
} |
|
|
|
|
|
|
|
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<PlayerCharacter> 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<AbstractWorldObject> 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()); |
|
} |
|
}
|
|
|