mirror of
https://github.com/ACEmulator/ACE
synced 2026-08-17 12:26:06 -04:00
510 lines
20 KiB
C#
510 lines
20 KiB
C#
using System;
|
|
|
|
using ACE.Common;
|
|
using ACE.Common.Extensions;
|
|
using ACE.Database.Models.Shard;
|
|
using ACE.Entity;
|
|
using ACE.Entity.Enum;
|
|
using ACE.Entity.Enum.Properties;
|
|
using ACE.Server.Entity;
|
|
using ACE.Server.Entity.Actions;
|
|
using ACE.Server.Managers;
|
|
using ACE.Server.Network.Enum;
|
|
using ACE.Server.Network.GameEvent.Events;
|
|
using ACE.Server.Network.GameMessages.Messages;
|
|
using ACE.Server.Network.Sequence;
|
|
using ACE.Server.Network.Structure;
|
|
|
|
namespace ACE.Server.WorldObjects
|
|
{
|
|
partial class Player
|
|
{
|
|
public void PlayerEnterWorld()
|
|
{
|
|
PlayerManager.SwitchPlayerFromOfflineToOnline(this);
|
|
Teleporting = true;
|
|
|
|
// Save the LoginTimestamp
|
|
var lastLoginTimestamp = Time.GetUnixTime();
|
|
|
|
LoginTimestamp = lastLoginTimestamp;
|
|
LastTeleportStartTimestamp = lastLoginTimestamp;
|
|
|
|
Character.LastLoginTimestamp = lastLoginTimestamp;
|
|
Character.TotalLogins++;
|
|
CharacterChangesDetected = true;
|
|
|
|
Sequences.SetSequence(SequenceType.ObjectInstance, new UShortSequence((ushort)Character.TotalLogins));
|
|
|
|
if (BarberActive)
|
|
BarberActive = false;
|
|
|
|
if (AllegianceNode != null)
|
|
AllegianceRank = (int)AllegianceNode.Rank;
|
|
else
|
|
AllegianceRank = null;
|
|
|
|
if (!Account15Days)
|
|
{
|
|
var accountAge = DateTime.UtcNow - Account.CreateTime;
|
|
|
|
if (accountAge.TotalDays >= 15)
|
|
Account15Days = true;
|
|
|
|
ManageAccount15Days_HousePurchaseTimestamp();
|
|
|
|
if (!Account15Days && IsOlthoiPlayer)
|
|
Session.Network.EnqueueSend(new GameMessageSystemChat("You may not leave Olthoi Island until your account and this character have been active on this game world for 15 days.", ChatMessageType.Broadcast));
|
|
}
|
|
|
|
if (PlayerKillerStatus == PlayerKillerStatus.PKLite && !PropertyManager.GetBool("pkl_server").Item)
|
|
{
|
|
PlayerKillerStatus = PlayerKillerStatus.NPK;
|
|
|
|
var actionChain = new ActionChain();
|
|
actionChain.AddDelaySeconds(3.0f);
|
|
actionChain.AddAction(this, () =>
|
|
{
|
|
Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreNonPKAgain));
|
|
});
|
|
actionChain.EnqueueChain();
|
|
}
|
|
|
|
HandlePreOrderItems();
|
|
|
|
// SendSelf will trigger the entrance into portal space
|
|
SendSelf();
|
|
|
|
// Update or override certain properties sent to client.
|
|
|
|
// bugged: do not send this here, or else a freshly loaded acclient will overrwrite the values
|
|
// wait until first enter world is completed
|
|
|
|
//SendPropertyUpdatesAndOverrides();
|
|
|
|
if (PropertyManager.GetBool("use_turbine_chat").Item)
|
|
{
|
|
// Init the client with the chat channel ID's, and then notify the player that they've joined the associated channels.
|
|
Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.TurbineChatIsEnabled));
|
|
|
|
if (IsOlthoiPlayer)
|
|
{
|
|
JoinTurbineChatChannel("Olthoi");
|
|
}
|
|
else
|
|
{
|
|
if (GetCharacterOption(CharacterOption.ListenToAllegianceChat) && Allegiance != null)
|
|
JoinTurbineChatChannel("Allegiance");
|
|
if (GetCharacterOption(CharacterOption.ListenToGeneralChat))
|
|
JoinTurbineChatChannel("General");
|
|
if (GetCharacterOption(CharacterOption.ListenToTradeChat))
|
|
JoinTurbineChatChannel("Trade");
|
|
if (GetCharacterOption(CharacterOption.ListenToLFGChat))
|
|
JoinTurbineChatChannel("LFG");
|
|
if (GetCharacterOption(CharacterOption.ListenToRoleplayChat))
|
|
JoinTurbineChatChannel("Roleplay");
|
|
if (GetCharacterOption(CharacterOption.ListenToSocietyChat) && Society != FactionBits.None)
|
|
JoinTurbineChatChannel("Society");
|
|
}
|
|
}
|
|
|
|
// check if vassals earned XP while offline
|
|
HandleAllegianceOnLogin();
|
|
HandleHouseOnLogin();
|
|
|
|
// retail appeared to send the squelch list very early,
|
|
// even before the CreatePlayer, but doing it here
|
|
if (SquelchManager.HasSquelches)
|
|
SquelchManager.SendSquelchDB();
|
|
|
|
AuditItemSpells();
|
|
AuditEquippedItems();
|
|
|
|
HandleMissingXp();
|
|
HandleSkillCreditRefund();
|
|
HandleSkillTemplesReset();
|
|
HandleSkillSpecCreditRefund();
|
|
HandleFreeSkillResetRenewal();
|
|
HandleFreeAttributeResetRenewal();
|
|
HandleFreeMasteryResetRenewal();
|
|
|
|
HandleDBUpdates();
|
|
|
|
if (ServerManager.ShutdownInitiated)
|
|
{
|
|
var actionChain = new ActionChain();
|
|
actionChain.AddDelaySeconds(10.0f);
|
|
actionChain.AddAction(this, () =>
|
|
{
|
|
SendMessage(ServerManager.ShutdownNoticeText(), ChatMessageType.WorldBroadcast);
|
|
});
|
|
actionChain.EnqueueChain();
|
|
}
|
|
|
|
log.DebugFormat("[LOGIN] Account {0} entered the world with character {1} (0x{2}) at {3}.", Account.AccountName, Name, Guid, DateTime.Now.ToCommonString());
|
|
}
|
|
|
|
public void SendTurbineChatChannels(bool breakAllegiance = false)
|
|
{
|
|
var allegianceChannel = Allegiance != null && !breakAllegiance ? Allegiance.Biota.Id : 0u;
|
|
|
|
var societyChannel = Society switch
|
|
{
|
|
FactionBits.CelestialHand => TurbineChatChannel.SocietyCelestialHand,
|
|
FactionBits.EldrytchWeb => TurbineChatChannel.SocietyEldrytchWeb,
|
|
FactionBits.RadiantBlood => TurbineChatChannel.SocietyRadiantBlood,
|
|
_ => 0u
|
|
};
|
|
|
|
Session.Network.EnqueueSend(new GameEventSetTurbineChatChannels(Session, allegianceChannel, societyChannel));
|
|
}
|
|
|
|
public void JoinTurbineChatChannel(string channelName)
|
|
{
|
|
if (channelName == "Allegiance" && Allegiance == null)
|
|
return;
|
|
else if (channelName == "Society")
|
|
{
|
|
if (Society == FactionBits.None)
|
|
return;
|
|
|
|
channelName = Society switch
|
|
{
|
|
FactionBits.CelestialHand => "Celestial Hand",
|
|
FactionBits.EldrytchWeb => "Eldrytch Web",
|
|
FactionBits.RadiantBlood => "Radiant Blood",
|
|
_ => channelName
|
|
};
|
|
}
|
|
else if (channelName == "Olthoi" && !IsOlthoiPlayer)
|
|
return;
|
|
|
|
Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(Session, WeenieErrorWithString.YouHaveEnteredThe_Channel, channelName));
|
|
|
|
SendTurbineChatChannels();
|
|
}
|
|
|
|
public void LeaveTurbineChatChannel(string channelName, bool breakAllegiance = false)
|
|
{
|
|
if (channelName == "Allegiance" && !breakAllegiance && Allegiance == null)
|
|
return;
|
|
else if (channelName == "Society")
|
|
{
|
|
if (Society == FactionBits.None)
|
|
return;
|
|
|
|
channelName = Society switch
|
|
{
|
|
FactionBits.CelestialHand => "Celestial Hand",
|
|
FactionBits.EldrytchWeb => "Eldrytch Web",
|
|
FactionBits.RadiantBlood => "Radiant Blood",
|
|
_ => channelName
|
|
};
|
|
}
|
|
else if (channelName == "Olthoi" && (!IsOlthoiPlayer || !IsAdmin))
|
|
return;
|
|
else if (IsOlthoiPlayer && !IsAdmin && channelName != "Olthoi")
|
|
return;
|
|
|
|
Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(Session, WeenieErrorWithString.YouHaveLeftThe_Channel, channelName));
|
|
|
|
SendTurbineChatChannels(breakAllegiance);
|
|
}
|
|
|
|
private void SendSelf()
|
|
{
|
|
var player = new GameEventPlayerDescription(Session);
|
|
var title = new GameEventCharacterTitle(Session);
|
|
var friends = new GameEventFriendsListUpdate(Session);
|
|
|
|
Session.Network.EnqueueSend(player, title, friends);
|
|
|
|
// Player objects don't get a placement
|
|
Placement = null;
|
|
Session.Network.EnqueueSend(new GameMessagePlayerCreate(Guid), new GameMessageCreateObject(this));
|
|
|
|
SendInventoryAndWieldedItems();
|
|
|
|
SendContractTrackerTable();
|
|
}
|
|
|
|
public void SendPropertyUpdatesAndOverrides()
|
|
{
|
|
if (!PropertyManager.GetBool("require_spell_comps").Item)
|
|
Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyBool(this, PropertyBool.SpellComponentsRequired, false));
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method iterates through your main pack, any packs and finds all the items contained
|
|
/// It also iterates over your wielded items - it sends create object messages needed by the login process
|
|
/// it is called from SendSelf as part of the login message traffic. Og II
|
|
/// </summary>
|
|
public void SendInventoryAndWieldedItems()
|
|
{
|
|
foreach (var item in Inventory.Values)
|
|
{
|
|
Session.Network.EnqueueSend(new GameMessageCreateObject(item));
|
|
|
|
// Was the item I just send a container? If so, we need to send the items in the container as well. Og II
|
|
if (item is Container container)
|
|
{
|
|
Session.Network.EnqueueSend(new GameEventViewContents(Session, container));
|
|
|
|
foreach (var itemsInContainer in container.Inventory.Values)
|
|
Session.Network.EnqueueSend(new GameMessageCreateObject(itemsInContainer));
|
|
}
|
|
}
|
|
|
|
foreach (var item in EquippedObjects.Values)
|
|
{
|
|
item.Wielder = this;
|
|
Session.Network.EnqueueSend(new GameMessageCreateObject(item));
|
|
}
|
|
}
|
|
|
|
public void SendContractTrackerTable()
|
|
{
|
|
if (Character.GetContractsCount(CharacterDatabaseLock) > 0)
|
|
Session.Network.EnqueueSend(new GameEventSendClientContractTrackerTable(Session));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will send out GameEventFriendsListUpdate packets to everyone online that has this player as a friend.
|
|
/// </summary>
|
|
public void SendFriendStatusUpdates(bool previouslyOnline, bool isOnline)
|
|
{
|
|
var appearOffline = GetAppearOffline();
|
|
var previouslyOnlineAndIsNowOffline = previouslyOnline && !isOnline;
|
|
var previouslyOfflineAndIsNowOnline = !previouslyOnline && isOnline;
|
|
var previouslyOfflineAndAppearOffline = !previouslyOnline && appearOffline;
|
|
|
|
if ((previouslyOfflineAndIsNowOnline && !previouslyOfflineAndAppearOffline) || previouslyOnlineAndIsNowOffline)
|
|
{
|
|
var msg = $"{Name} has {(isOnline ? "come on" : "gone off")}line.";
|
|
|
|
var inverseFriends = PlayerManager.GetOnlineInverseFriends(Guid);
|
|
|
|
foreach (var friend in inverseFriends)
|
|
{
|
|
var playerFriend = new CharacterPropertiesFriendList { CharacterId = friend.Guid.Full, FriendId = Guid.Full };
|
|
friend.Session.Network.EnqueueSend(new GameEventFriendsListUpdate(friend.Session, GameEventFriendsListUpdate.FriendsUpdateTypeFlag.FriendStatusChanged, playerFriend, true, isOnline));
|
|
friend.SendMessage(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Records where the client thinks we are, for use by physics engine later
|
|
/// </summary>
|
|
public void SetRequestedLocation(Position pos, bool broadcast = true)
|
|
{
|
|
RequestedLocation = pos;
|
|
RequestedLocationBroadcast = broadcast;
|
|
}
|
|
|
|
public MotionCommand LastSoulEmote;
|
|
public DateTime LastSoulEmoteEndTime;
|
|
|
|
public void BroadcastMovement(MoveToState moveToState)
|
|
{
|
|
var state = moveToState.RawMotionState;
|
|
|
|
// update current style
|
|
if ((state.Flags & RawMotionFlags.CurrentStyle) != 0)
|
|
{
|
|
// this lowercase stance field in Player doesn't really seem to be used anywhere
|
|
stance = state.CurrentStyle;
|
|
}
|
|
|
|
// update CurrentMotionState here for substates?
|
|
if ((state.Flags & RawMotionFlags.ForwardCommand) != 0)
|
|
{
|
|
if (((uint)state.ForwardCommand & (uint)CommandMask.SubState) != 0)
|
|
CurrentMotionState.SetForwardCommand(state.ForwardCommand);
|
|
}
|
|
else
|
|
CurrentMotionState.SetForwardCommand(MotionCommand.Ready);
|
|
|
|
if (state.CommandListLength > 0)
|
|
{
|
|
if (((uint)state.Commands[0].MotionCommand & (uint)CommandMask.SubState) != 0)
|
|
CurrentMotionState.SetForwardCommand(state.Commands[0].MotionCommand);
|
|
}
|
|
|
|
if (state.HasSoulEmote(false))
|
|
{
|
|
// prevent soul emote spam / bug where client sends multiples
|
|
var soulEmote = state.Commands[0].MotionCommand;
|
|
if (soulEmote == LastSoulEmote && DateTime.UtcNow < LastSoulEmoteEndTime)
|
|
{
|
|
state.Commands.Clear();
|
|
state.CommandListLength = 0;
|
|
}
|
|
else
|
|
{
|
|
var animLength = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, soulEmote, state.Commands[0].Speed);
|
|
|
|
LastSoulEmote = soulEmote;
|
|
LastSoulEmoteEndTime = DateTime.UtcNow + TimeSpan.FromSeconds(animLength);
|
|
}
|
|
}
|
|
|
|
var movementData = new MovementData(this, moveToState);
|
|
|
|
// copy some fields to CurrentMotionState?
|
|
// this is a mess, fix this whole architecture.
|
|
CurrentMotionState.MotionState.ForwardCommand = movementData.Invalid.State.ForwardCommand;
|
|
CurrentMotionState.MotionState.ForwardSpeed = movementData.Invalid.State.ForwardSpeed;
|
|
CurrentMotionState.MotionState.TurnCommand = movementData.Invalid.State.TurnCommand;
|
|
CurrentMotionState.MotionState.TurnSpeed = movementData.Invalid.State.TurnSpeed;
|
|
CurrentMotionState.MotionState.SidestepCommand = movementData.Invalid.State.SidestepCommand;
|
|
CurrentMotionState.MotionState.SidestepSpeed = movementData.Invalid.State.SidestepSpeed;
|
|
|
|
var movementEvent = new GameMessageUpdateMotion(this, movementData);
|
|
EnqueueBroadcast(true, movementEvent); // shouldn't need to go to originating player?
|
|
|
|
// TODO: use real motion / animation system from physics
|
|
//CurrentMotionCommand = movementData.Invalid.State.ForwardCommand;
|
|
CurrentMovementData = movementData;
|
|
}
|
|
|
|
private EnvironChangeType? currentFogColor;
|
|
|
|
public void SetFogColor(EnvironChangeType fogColor)
|
|
{
|
|
if (fogColor == EnvironChangeType.Clear && !currentFogColor.HasValue)
|
|
return;
|
|
|
|
if (LandblockManager.GlobalFogColor.HasValue && currentFogColor != fogColor)
|
|
{
|
|
currentFogColor = LandblockManager.GlobalFogColor;
|
|
SendEnvironChange(currentFogColor.Value);
|
|
}
|
|
else if (currentFogColor != fogColor)
|
|
{
|
|
currentFogColor = fogColor;
|
|
SendEnvironChange(currentFogColor.Value);
|
|
}
|
|
|
|
if (currentFogColor == EnvironChangeType.Clear)
|
|
currentFogColor = null;
|
|
}
|
|
|
|
public void ClearFogColor()
|
|
{
|
|
SetFogColor(EnvironChangeType.Clear);
|
|
}
|
|
|
|
public void SendEnvironChange(EnvironChangeType environChangeType)
|
|
{
|
|
Session.Network.EnqueueSend(new GameMessageAdminEnvirons(Session, environChangeType));
|
|
}
|
|
|
|
public void SetPlayerKillerStatus(PlayerKillerStatus playerKillerStatus, bool broadcast = false)
|
|
{
|
|
switch (playerKillerStatus)
|
|
{
|
|
case PlayerKillerStatus.NPK:
|
|
case PlayerKillerStatus.PK:
|
|
case PlayerKillerStatus.PKLite:
|
|
PlayerKillerStatus = PlayerKillerStatus.NPK;
|
|
MinimumTimeSincePk = 0;
|
|
break;
|
|
case PlayerKillerStatus.Free:
|
|
PlayerKillerStatus = PlayerKillerStatus.Free;
|
|
break;
|
|
}
|
|
|
|
if (broadcast)
|
|
EnqueueBroadcast(new GameMessagePublicUpdatePropertyInt(this, PropertyInt.PlayerKillerStatus, (int)PlayerKillerStatus));
|
|
}
|
|
|
|
public void SendWeenieError(WeenieError error)
|
|
{
|
|
Session.Network.EnqueueSend(new GameEventWeenieError(Session, error));
|
|
}
|
|
|
|
public void SendWeenieErrorWithString(WeenieErrorWithString error, string str)
|
|
{
|
|
Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(Session, error, str));
|
|
}
|
|
|
|
public void SendTransientError(string msg)
|
|
{
|
|
Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, msg));
|
|
}
|
|
|
|
public void HandleActionSetAFKMode(bool afkStatus)
|
|
{
|
|
IsAfk = afkStatus;
|
|
|
|
Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyBool(this, PropertyBool.Afk, IsAfk));
|
|
}
|
|
|
|
public static string DefaultAFKMessage => "I am currently away from the keyboard."; // client default (/afk msg)
|
|
|
|
public void HandleActionSetAFKMessage(string afkMessage)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(afkMessage))
|
|
afkMessage = DefaultAFKMessage; // client default
|
|
|
|
AfkMessage = afkMessage;
|
|
}
|
|
|
|
public void HandlePreOrderItems()
|
|
{
|
|
var subscriptionStatus = (SubscriptionStatus)PropertyManager.GetLong("default_subscription_level").Item;
|
|
|
|
string status;
|
|
bool success;
|
|
switch (subscriptionStatus)
|
|
{
|
|
default:
|
|
status = "purchasing";
|
|
success = TryCreatePreOrderItem(PropertyBool.ActdReceivedItems, ACE.Entity.Enum.WeenieClassName.W_GEMACTDPURCHASEREWARDARMOR_CLASS);
|
|
break;
|
|
case SubscriptionStatus.ThroneOfDestiny_Preordered:
|
|
status = "pre-ordering";
|
|
TryCreatePreOrderItem(PropertyBool.ActdReceivedItems, ACE.Entity.Enum.WeenieClassName.W_GEMACTDPURCHASEREWARDARMOR_CLASS); // pcaps show this actually didn't occur on retail. odd
|
|
success = TryCreatePreOrderItem(PropertyBool.ActdPreorderReceivedItems, ACE.Entity.Enum.WeenieClassName.W_GEMACTDPURCHASEREWARDHEALTH_CLASS);
|
|
break;
|
|
}
|
|
|
|
var msg = $"Thank you for {status} the Throne of Destiny expansion! A special gift has been placed in your backpack.";
|
|
|
|
if (PropertyManager.GetBool("show_first_login_gift").Item && success)
|
|
Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Magic));
|
|
|
|
AccountRequirements = subscriptionStatus;
|
|
}
|
|
|
|
private bool TryCreatePreOrderItem(PropertyBool propertyBool, WeenieClassName weenieClassName)
|
|
{
|
|
var rcvdBlackmoorsFavor = GetProperty(propertyBool) ?? false;
|
|
if (!rcvdBlackmoorsFavor)
|
|
{
|
|
if (GetInventoryItemsOfWCID((uint)weenieClassName).Count == 0)
|
|
{
|
|
var cachedWeenie = Database.DatabaseManager.World.GetCachedWeenie((uint)weenieClassName);
|
|
if (cachedWeenie == null)
|
|
return false;
|
|
|
|
var wo = Factories.WorldObjectFactory.CreateNewWorldObject(cachedWeenie);
|
|
if (wo == null)
|
|
return false;
|
|
|
|
if (TryAddToInventory(wo))
|
|
{
|
|
SetProperty(propertyBool, true);
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
SetProperty(propertyBool, true); // already had the item, set the property to reflect item was received
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|