using System; using System.Collections.Generic; using System.Numerics; using System.Linq; using ACE.Common; using ACE.Database; using ACE.DatLoader; using ACE.DatLoader.FileTypes; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.Managers; namespace ACE.Server.WorldObjects { partial class Player { private static readonly Position MarketplaceDrop = DatabaseManager.World.GetCachedWeenie("portalmarketplace")?.GetPosition(PositionType.Destination) ?? new Position(0x016C01BC, 49.206f, -31.935f, 0.005f, 0, 0, -0.707107f, 0.707107f); /// /// Teleports the player to position /// /// PositionType to be teleported to /// true on success (position is set) false otherwise public bool TeleToPosition(PositionType positionType) { var position = GetPosition(positionType); if (position != null) { var teleportDest = new Position(position); AdjustDungeon(teleportDest); Teleport(teleportDest); return true; } return false; } private static readonly Motion motionLifestoneRecall = new Motion(MotionStance.NonCombat, MotionCommand.LifestoneRecall); private static readonly Motion motionHouseRecall = new Motion(MotionStance.NonCombat, MotionCommand.HouseRecall); public static float RecallMoveThreshold = 8.0f; public static float RecallMoveThresholdSq = RecallMoveThreshold * RecallMoveThreshold; public bool TooBusyToRecall { get => IsBusy || suicideInProgress; // recalls could be started from portal space? } public void HandleActionTeleToHouse() { if (IsOlthoiPlayer) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OlthoiCanOnlyRecallToLifestone)); return; } if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } var house = House ?? GetAccountHouse(); if (house == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouMustOwnHouseToUseCommand)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling home.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.HouseRecall, MotionStance.NonCombat); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.HouseRecall); actionChain.AddDelaySeconds(animLength); IsBusy = true; actionChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(house.SlumLord.Location); }); actionChain.EnqueueChain(); } /// /// Handles teleporting a player to the lifestone (/ls or /lifestone command) /// public void HandleActionTeleToLifestone() { if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } if (Sanctuary == null) { Session.Network.EnqueueSend(new GameMessageSystemChat("Your spirit has not been attuned to a sanctuary location.", ChatMessageType.Broadcast)); return; } // FIXME(ddevec): I should probably make a better interface for this UpdateVital(Mana, Mana.Current / 2); if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the lifestone.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.LifestoneRecall, MotionStance.NonCombat); var startPos = new Position(Location); // Wait for animation ActionChain lifestoneChain = new ActionChain(); // Then do teleport IsBusy = true; lifestoneChain.AddDelaySeconds(DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.LifestoneRecall)); lifestoneChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(Sanctuary); }); lifestoneChain.EnqueueChain(); } private static readonly Motion motionMarketplaceRecall = new Motion(MotionStance.NonCombat, MotionCommand.MarketplaceRecall); public void HandleActionTeleToMarketPlace() { if (IsOlthoiPlayer) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OlthoiCanOnlyRecallToLifestone)); return; } if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the marketplace.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.MarketplaceRecall, MotionStance.NonCombat); var startPos = new Position(Location); // TODO: (OptimShi): Actual animation length is longer than in retail. 18.4s // float mpAnimationLength = MotionTable.GetAnimationLength((uint)MotionTableId, MotionCommand.MarketplaceRecall); // mpChain.AddDelaySeconds(mpAnimationLength); ActionChain mpChain = new ActionChain(); mpChain.AddDelaySeconds(14); // Then do teleport IsBusy = true; mpChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(MarketplaceDrop); }); // Set the chain to run mpChain.EnqueueChain(); } private static readonly Motion motionAllegianceHometownRecall = new Motion(MotionStance.NonCombat, MotionCommand.AllegianceHometownRecall); public void HandleActionRecallAllegianceHometown() { //Console.WriteLine($"{Name}.HandleActionRecallAllegianceHometown()"); if (IsOlthoiPlayer) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OlthoiCanOnlyRecallToLifestone)); return; } if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } // check if player is in an allegiance if (!VerifyRecallAllegianceHometown()) return; if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is going to the Allegiance hometown.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.AllegianceHometownRecall, MotionStance.NonCombat); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport IsBusy = true; var animLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.AllegianceHometownRecall); actionChain.AddDelaySeconds(animLength); actionChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } // re-verify if (!VerifyRecallAllegianceHometown()) return; Teleport(Allegiance.Sanctuary); }); actionChain.EnqueueChain(); } private bool VerifyRecallAllegianceHometown() { if (Allegiance == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreNotInAllegiance)); return false; } if (Allegiance.Sanctuary == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourAllegianceDoesNotHaveHometown)); return false; } return true; } /// /// Recalls you to your allegiance's Mansion or Villa /// public void HandleActionTeleToMansion() { //Console.WriteLine($"{Name}.HandleActionTeleToMansion()"); if (IsOlthoiPlayer) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OlthoiCanOnlyRecallToLifestone)); return; } if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } var allegianceHouse = VerifyTeleToMansion(); if (allegianceHouse == null) return; if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the Allegiance housing.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.HouseRecall, MotionStance.NonCombat); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.HouseRecall); actionChain.AddDelaySeconds(animLength); IsBusy = true; actionChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } // re-verify allegianceHouse = VerifyTeleToMansion(); if (allegianceHouse == null) return; Teleport(allegianceHouse.SlumLord.Location); }); actionChain.EnqueueChain(); } private House VerifyTeleToMansion() { // check if player is in an allegiance if (Allegiance == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreNotInAllegiance)); return null; } var allegianceHouse = Allegiance.GetHouse(); if (allegianceHouse == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourMonarchDoesNotOwnAMansionOrVilla)); return null; } if (allegianceHouse.HouseType != HouseType.Villa && allegianceHouse.HouseType != HouseType.Mansion) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourMonarchsHouseIsNotAMansionOrVilla)); return null; } // ensure allegiance housing has allegiance permissions enabled if (allegianceHouse.MonarchId == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourMonarchHasClosedTheMansion)); return null; } return allegianceHouse; } private static readonly Motion motionPkArenaRecall = new Motion(MotionStance.NonCombat, MotionCommand.PKArenaRecall); private static List pkArenaLocs = new List() { new Position(DatabaseManager.World.GetCachedWeenie("portalpkarenanew1")?.GetPosition(PositionType.Destination) ?? new Position(0x00660117, 30, -50, 0.005f, 0, 0, 0.000000f, 1.000000f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpkarenanew2")?.GetPosition(PositionType.Destination) ?? new Position(0x00660106, 10, 0, 0.005f, 0, 0, -0.947071f, 0.321023f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpkarenanew3")?.GetPosition(PositionType.Destination) ?? new Position(0x00660103, 30, -30, 0.005f, 0, 0, -0.699713f, 0.714424f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpkarenanew4")?.GetPosition(PositionType.Destination) ?? new Position(0x0066011E, 50, 0, 0.005f, 0, 0, -0.961021f, -0.276474f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpkarenanew5")?.GetPosition(PositionType.Destination) ?? new Position(0x00660127, 60, -30, 0.005f, 0, 0, 0.681639f, 0.731689f)), }; public void HandleActionTeleToPkArena() { //Console.WriteLine($"{Name}.HandleActionTeleToPkArena()"); if (PlayerKillerStatus != PlayerKillerStatus.PK) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OnlyPKsMayUseCommand)); return; } if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is going to the PK Arena.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.PKArenaRecall, MotionStance.NonCombat); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.PKArenaRecall); actionChain.AddDelaySeconds(animLength); IsBusy = true; actionChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } var rng = ThreadSafeRandom.Next(0, pkArenaLocs.Count - 1); var loc = pkArenaLocs[rng]; Teleport(loc); }); actionChain.EnqueueChain(); } private static List pklArenaLocs = new List() { new Position(DatabaseManager.World.GetCachedWeenie("portalpklarenanew1")?.GetPosition(PositionType.Destination) ?? new Position(0x00670117, 30, -50, 0.005f, 0, 0, 0.000000f, 1.000000f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpklarenanew2")?.GetPosition(PositionType.Destination) ?? new Position(0x00670106, 10, 0, 0.005f, 0, 0, -0.947071f, 0.321023f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpklarenanew3")?.GetPosition(PositionType.Destination) ?? new Position(0x00670103, 30, -30, 0.005f, 0, 0, -0.699713f, 0.714424f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpklarenanew4")?.GetPosition(PositionType.Destination) ?? new Position(0x0067011E, 50, 0, 0.005f, 0, 0, -0.961021f, -0.276474f)), new Position(DatabaseManager.World.GetCachedWeenie("portalpklarenanew5")?.GetPosition(PositionType.Destination) ?? new Position(0x00670127, 60, -30, 0.005f, 0, 0, 0.681639f, 0.731689f)), }; public void HandleActionTeleToPklArena() { //Console.WriteLine($"{Name}.HandleActionTeleToPkLiteArena()"); if (IsOlthoiPlayer) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OlthoiCanOnlyRecallToLifestone)); return; } if (PlayerKillerStatus != PlayerKillerStatus.PKLite) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.OnlyPKLiteMayUseCommand)); return; } if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (TooBusyToRecall) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is going to the PKL Arena.", ChatMessageType.Recall), LocalBroadcastRange, ChatMessageType.Recall); SendMotionAsCommands(MotionCommand.PKArenaRecall, MotionStance.NonCombat); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.PKArenaRecall); actionChain.AddDelaySeconds(animLength); IsBusy = true; actionChain.AddAction(this, () => { IsBusy = false; var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } var rng = ThreadSafeRandom.Next(0, pklArenaLocs.Count - 1); var loc = pklArenaLocs[rng]; Teleport(loc); }); actionChain.EnqueueChain(); } public void SendMotionAsCommands(MotionCommand motionCommand, MotionStance motionStance) { if (FastTick) { var actionChain = new ActionChain(); EnqueueMotionAction(actionChain, new List() { motionCommand }, 1.0f, motionStance); actionChain.EnqueueChain(); } else { var motion = new Motion(motionStance, MotionCommand.Ready); motion.MotionState.AddCommand(this, motionCommand); EnqueueBroadcastMotion(motion); } } public DateTime LastTeleportTime; /// /// This is not thread-safe. Consider using WorldManager.ThreadSafeTeleport() instead if you're calling this from a multi-threaded subsection. /// public void Teleport(Position _newPosition, bool fromPortal = false) { var newPosition = new Position(_newPosition); //newPosition.PositionZ += 0.005f; newPosition.PositionZ += 0.005f * (ObjScale ?? 1.0f); //Console.WriteLine($"{Name}.Teleport() - Sending to {newPosition.ToLOCString()}"); // Check currentFogColor set for player. If LandblockManager.GlobalFogColor is set, don't bother checking, dungeons didn't clear like this on retail worlds. // if not clear, reset to clear before portaling in case portaling to dungeon (no current way to fast check unloaded landblock for IsDungeon or current FogColor) // client doesn't respond to any change inside dungeons, and only queues for change if in dungeon, executing change upon next teleport // so if we delay teleport long enough to ensure clear arrives before teleport, we don't get fog carrying over into dungeon. if (currentFogColor.HasValue && currentFogColor != EnvironChangeType.Clear && !LandblockManager.GlobalFogColor.HasValue) { var delayTelport = new ActionChain(); delayTelport.AddAction(this, () => ClearFogColor()); delayTelport.AddDelaySeconds(1); delayTelport.AddAction(this, () => WorldManager.ThreadSafeTeleport(this, _newPosition)); delayTelport.EnqueueChain(); return; } Teleporting = true; LastTeleportTime = DateTime.UtcNow; LastTeleportStartTimestamp = Time.GetUnixTime(); if (fromPortal) LastPortalTeleportTimestamp = LastTeleportStartTimestamp; Session.Network.EnqueueSend(new GameMessagePlayerTeleport(this)); // load quickly, but player can load into landblock before server is finished loading // send a "fake" update position to get the client to start loading asap, // also might fix some decal bugs var prevLoc = Location; Location = newPosition; SendUpdatePosition(); Location = prevLoc; DoTeleportPhysicsStateChanges(); // force out of hotspots PhysicsObj.report_collision_end(true); if (UnderLifestoneProtection) LifestoneProtectionDispel(); HandlePreTeleportVisibility(newPosition); UpdatePlayerPosition(new Position(newPosition), true); } public void DoPreTeleportHide() { if (Teleporting) return; PlayParticleEffect(PlayScript.Hide, Guid); } public void DoTeleportPhysicsStateChanges() { var broadcastUpdate = false; var oldHidden = Hidden.Value; var oldIgnore = IgnoreCollisions.Value; var oldReport = ReportCollisions.Value; Hidden = true; IgnoreCollisions = true; ReportCollisions = false; if (Hidden != oldHidden || IgnoreCollisions != oldIgnore || ReportCollisions != oldReport) broadcastUpdate = true; if (broadcastUpdate) EnqueueBroadcastPhysicsState(); } /// /// Prevent message spam /// public double? LastPortalTeleportTimestampError; public void OnTeleportComplete() { if (CurrentLandblock != null && !CurrentLandblock.CreateWorldObjectsCompleted) { // If the critical landblock resources haven't been loaded yet, we keep the player in the pink bubble state // We'll check periodically to see when it's safe to let them materialize in var actionChain = new ActionChain(); actionChain.AddDelaySeconds(0.1); actionChain.AddAction(this, OnTeleportComplete); actionChain.EnqueueChain(); return; } // set materialize physics state // this takes the player from pink bubbles -> fully materialized if (CloakStatus != CloakStatus.On) ReportCollisions = true; IgnoreCollisions = false; Hidden = false; Teleporting = false; CheckMonsters(); CheckHouse(); EnqueueBroadcastPhysicsState(); // hijacking this for both start/end on portal teleport if (LastTeleportStartTimestamp == LastPortalTeleportTimestamp) LastPortalTeleportTimestamp = Time.GetUnixTime(); } public void SendTeleportedViaMagicMessage(WorldObject itemCaster, Spell spell) { if (itemCaster == null || itemCaster is Gem) Session.Network.EnqueueSend(new GameMessageSystemChat($"You have been teleported.", ChatMessageType.Magic)); else if (this != itemCaster && !(itemCaster is Gem) && !(itemCaster is Switch) && !(itemCaster.GetProperty(PropertyBool.NpcInteractsSilently) ?? false)) Session.Network.EnqueueSend(new GameMessageSystemChat($"{itemCaster.Name} teleports you with {spell.Name}.", ChatMessageType.Magic)); //else if (itemCaster is Gem) // Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ITeleported)); } public void NotifyLandblocks() { // the original implementations of this were done on landblock heartbeat, // with checks for players in the current landblock, as well as adjacent outdoor landblocks // for performance reasons, this is being reimplemented in the reverse manner, // with players notifying landblocks of their activity // notify current landblock of player activity if (CurrentLandblock != null) CurrentLandblock?.SetActive(); } public const float RunFactor = 1.5f; /// /// Returns the amount of time for player to rotate by the # of degrees /// from the input angle, using the omega speed from its MotionTable /// public override float GetRotateDelay(float angle) { return base.GetRotateDelay(angle) / RunFactor; } /// /// A list of landblocks the player cannot relog directly into /// /// If a regular player logs out in one of these landblocks, /// they will be transported back to the lifestone when they log back in. /// public static HashSet NoLog_Landblocks = new HashSet() { // https://asheron.fandom.com/wiki/Special:Search?query=Lifestone+on+Relog%3A+Yes+ // https://docs.google.com/spreadsheets/d/122xOw3IKCezaTDjC_hggWSVzYJ_9M_zUUtGEXkwNXfs/edit#gid=846612575 0x0002, // Viamontian Garrison 0x0007, // Town Network 0x0056, // Augmentation Realm Main Level 0x005F, // Tanada House of Pancakes (Seasonal) 0x0067, // PKL Arena 0x006D, // Augmentation Realm Upper Level 0x007D, // Augmentation Realm Lower Level 0x00AB, // Derethian Combat Arena 0x00AC, // Derethian Combat Arena 0x00C3, // Blighted Putrid Moarsman Tunnels 0x00D7, // Jester's Prison 0x00EA, // Mhoire Armory 0x015D, // Mountain Cavern 0x027F, // East Fork Dam Hive 0x03A7, // Mount Elyrii Hive 0x5764, // Oubliette of Mhoire Castle 0x634C, // Tainted Grotto 0x6544, // Greater Battle Dungeon 0x6651, // Hoshino Tower 0x7E04, // Thug Hideout 0x8A04, // Night Club (Seasonal Anniversary) 0x8B04, // Frozen Wight Lair 0x9EE5, // Northwatch Castle Black Market 0xB5F0, // Aerfalle's Sanctum 0xF92F, // Freebooter Keep Black Market 0x00B0, // Colosseum Arena One 0x00B1, // Colosseum Arena Two 0x00B2, // Colosseum Arena Three 0x00B3, // Colosseum Arena Four 0x00B4, // Colosseum Arena Five 0x00B6, // Colosseum Arena Mini-Bosses 0x5960, // Gauntlet Arena One (Celestial Hand) 0x5961, // Gauntlet Arena Two (Celestial Hand) 0x5962, // Gauntlet Arena One (Eldritch Web) 0x5963, // Gauntlet Arena Two (Eldritch Web) 0x5964, // Gauntlet Arena One (Radiant Blood) 0x5965, // Gauntlet Arena Two (Radiant Blood) }; /// /// Called when a player first logs in /// public static void HandleNoLogLandblock(Biota biota, out bool playerWasMovedFromNoLogLandblock) { playerWasMovedFromNoLogLandblock = false; if (biota.WeenieType == WeenieType.Sentinel || biota.WeenieType == WeenieType.Admin) return; if (!biota.PropertiesPosition.TryGetValue(PositionType.Location, out var location)) return; var landblock = (ushort)(location.ObjCellId >> 16); if (!NoLog_Landblocks.Contains(landblock)) return; if (!biota.PropertiesPosition.TryGetValue(PositionType.Sanctuary, out var lifestone)) return; location.ObjCellId = lifestone.ObjCellId; location.PositionX = lifestone.PositionX; location.PositionY = lifestone.PositionY; location.PositionZ = lifestone.PositionZ; location.RotationX = lifestone.RotationX; location.RotationY = lifestone.RotationY; location.RotationZ = lifestone.RotationZ; location.RotationW = lifestone.RotationW; playerWasMovedFromNoLogLandblock = true; return; } } }