Rewrite trainer handling to properly support multiple trainers on the same creature
This commit is contained in:
@@ -860,11 +860,9 @@ namespace Game.Entities
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool isCanTrainingAndResetTalentsOf(Player player)
|
||||
public bool CanResetTalents(Player player)
|
||||
{
|
||||
return player.getLevel() >= 10
|
||||
&& GetCreatureTemplate().TrainerType == TrainerType.Class
|
||||
&& player.GetClass() == GetCreatureTemplate().TrainerClass;
|
||||
return player.getLevel() >= 15 && player.GetClass() == GetCreatureTemplate().TrainerClass;
|
||||
}
|
||||
|
||||
public void SetTextRepeatId(byte textGroup, byte id)
|
||||
@@ -2457,11 +2455,6 @@ namespace Game.Entities
|
||||
return vCount.count;
|
||||
}
|
||||
|
||||
public TrainerSpellData GetTrainerSpells()
|
||||
{
|
||||
return Global.ObjectMgr.GetNpcTrainerSpells(GetEntry());
|
||||
}
|
||||
|
||||
public override string GetName(LocaleConstant locale_idx = LocaleConstant.enUS)
|
||||
{
|
||||
if (locale_idx != LocaleConstant.enUS)
|
||||
|
||||
@@ -58,9 +58,7 @@ namespace Game.Entities
|
||||
public uint UnitFlags3;
|
||||
public uint DynamicFlags;
|
||||
public CreatureFamily Family;
|
||||
public TrainerType TrainerType;
|
||||
public Class TrainerClass;
|
||||
public Race TrainerRace;
|
||||
public CreatureType CreatureType;
|
||||
public CreatureTypeFlags TypeFlags;
|
||||
public uint TypeFlags2;
|
||||
@@ -386,38 +384,4 @@ namespace Game.Entities
|
||||
m_items.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public class TrainerSpell
|
||||
{
|
||||
public uint SpellID;
|
||||
public uint MoneyCost;
|
||||
public uint ReqSkillLine;
|
||||
public uint ReqSkillRank;
|
||||
public uint ReqLevel;
|
||||
public uint[] ReqAbility = new uint[SharedConst.MaxTrainerspellAbilityReqs];
|
||||
public uint Index;
|
||||
|
||||
// helpers
|
||||
public bool IsCastable()
|
||||
{
|
||||
return ReqAbility[0] != SpellID;
|
||||
}
|
||||
}
|
||||
|
||||
public class TrainerSpellData
|
||||
{
|
||||
public TrainerSpellData()
|
||||
{
|
||||
trainerType = 0;
|
||||
}
|
||||
|
||||
public Dictionary<uint, TrainerSpell> spellList = new Dictionary<uint, TrainerSpell>();
|
||||
public uint trainerType; // trainer type based at trainer spells, can be different from creature_template value.
|
||||
// req. for correct show non-prof. trainers like weaponmaster, allowed values 0 and 2.
|
||||
|
||||
public TrainerSpell Find(uint spell_id)
|
||||
{
|
||||
return spellList.LookupByKey(spell_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,22 +30,22 @@ namespace Game.Misc
|
||||
{
|
||||
public class GossipMenu
|
||||
{
|
||||
public void AddMenuItem(int menuItemId, GossipOptionIcon icon, string message, uint sender, uint action, string boxMessage, uint boxMoney, bool coded = false)
|
||||
public uint AddMenuItem(int optionIndex, GossipOptionIcon icon, string message, uint sender, uint action, string boxMessage, uint boxMoney, bool coded = false)
|
||||
{
|
||||
Contract.Assert(_menuItems.Count <= SharedConst.MaxGossipMenuItems);
|
||||
|
||||
// Find a free new id - script case
|
||||
if (menuItemId == -1)
|
||||
if (optionIndex == -1)
|
||||
{
|
||||
menuItemId = 0;
|
||||
optionIndex = 0;
|
||||
if (!_menuItems.Empty())
|
||||
{
|
||||
foreach (var item in _menuItems)
|
||||
{
|
||||
if (item.Key > menuItemId)
|
||||
if (item.Key > optionIndex)
|
||||
break;
|
||||
|
||||
menuItemId = (int)item.Key + 1;
|
||||
optionIndex = (int)item.Key + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,8 @@ namespace Game.Misc
|
||||
menuItem.BoxMessage = boxMessage;
|
||||
menuItem.BoxMoney = boxMoney;
|
||||
|
||||
_menuItems[(uint)menuItemId] = menuItem;
|
||||
_menuItems[(uint)optionIndex] = menuItem;
|
||||
return (uint)optionIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,18 +125,20 @@ namespace Game.Misc
|
||||
}
|
||||
|
||||
// Add menu item with existing method. Menu item id -1 is also used in ADD_GOSSIP_ITEM macro.
|
||||
AddMenuItem(-1, item.OptionIcon, strOptionText, sender, action, strBoxText, item.BoxMoney, item.BoxCoded);
|
||||
uint optionIndex = AddMenuItem(-1, item.OptionIcon, strOptionText, sender, action, strBoxText, item.BoxMoney, item.BoxCoded);
|
||||
AddGossipMenuItemData(optionIndex, item.ActionMenuId, item.ActionPoiId, item.TrainerId);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddGossipMenuItemData(uint menuItemId, uint gossipActionMenuId, uint gossipActionPoi)
|
||||
public void AddGossipMenuItemData(uint optionIndex, uint gossipActionMenuId, uint gossipActionPoi, uint trainerId)
|
||||
{
|
||||
GossipMenuItemData itemData = new GossipMenuItemData();
|
||||
|
||||
itemData.GossipActionMenuId = gossipActionMenuId;
|
||||
itemData.GossipActionPoi = gossipActionPoi;
|
||||
itemData.TrainerId = trainerId;
|
||||
|
||||
_menuItemData[menuItemId] = itemData;
|
||||
_menuItemData[optionIndex] = itemData;
|
||||
}
|
||||
|
||||
public uint GetMenuItemSender(uint menuItemId)
|
||||
@@ -162,6 +165,15 @@ namespace Game.Misc
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasMenuItemType(uint optionType)
|
||||
{
|
||||
foreach (var menuItemPair in _menuItems)
|
||||
if (menuItemPair.Value.OptionType == optionType)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ClearMenu()
|
||||
{
|
||||
_menuItems.Clear();
|
||||
@@ -796,6 +808,7 @@ namespace Game.Misc
|
||||
{
|
||||
public uint GossipActionMenuId; // MenuId of the gossip triggered by this action
|
||||
public uint GossipActionPoi;
|
||||
public uint TrainerId;
|
||||
}
|
||||
|
||||
public struct NpcTextData
|
||||
@@ -829,6 +842,7 @@ namespace Game.Misc
|
||||
public uint BoxMoney;
|
||||
public string BoxText;
|
||||
public uint BoxBroadcastTextId;
|
||||
public uint TrainerId;
|
||||
public List<Condition> Conditions = new List<Condition>();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Framework.Constants;
|
||||
using Game.Network.Packets;
|
||||
using Game.Spells;
|
||||
|
||||
namespace Game.Entities
|
||||
{
|
||||
public class TrainerSpell
|
||||
{
|
||||
public uint SpellId;
|
||||
public uint MoneyCost;
|
||||
public uint ReqSkillLine;
|
||||
public uint ReqSkillRank;
|
||||
public Array<uint> ReqAbility = new Array<uint>(3);
|
||||
public byte ReqLevel;
|
||||
|
||||
public uint CastSpellId;
|
||||
}
|
||||
|
||||
public class Trainer
|
||||
{
|
||||
public Trainer(uint id, TrainerType type, string greeting, List<TrainerSpell> spells)
|
||||
{
|
||||
_id = id;
|
||||
_type = type;
|
||||
_spells = spells;
|
||||
|
||||
_greeting[(int)LocaleConstant.enUS] = greeting;
|
||||
}
|
||||
|
||||
public void SendSpells(Creature npc, Player player, LocaleConstant locale)
|
||||
{
|
||||
float reputationDiscount = player.GetReputationPriceDiscount(npc);
|
||||
|
||||
TrainerList trainerList = new TrainerList();
|
||||
trainerList.TrainerGUID = npc.GetGUID();
|
||||
trainerList.TrainerType = (int)_type;
|
||||
trainerList.TrainerID = (int)_id;
|
||||
trainerList.Greeting = GetGreeting(locale);
|
||||
|
||||
foreach (TrainerSpell trainerSpell in _spells)
|
||||
{
|
||||
if (!player.IsSpellFitByClassAndRace(trainerSpell.SpellId))
|
||||
continue;
|
||||
|
||||
TrainerListSpell trainerListSpell = new TrainerListSpell();
|
||||
trainerListSpell.SpellID = trainerSpell.SpellId;
|
||||
trainerListSpell.MoneyCost = (uint)(trainerSpell.MoneyCost * reputationDiscount);
|
||||
trainerListSpell.ReqSkillLine = trainerSpell.ReqSkillLine;
|
||||
trainerListSpell.ReqSkillRank = trainerSpell.ReqSkillRank;
|
||||
trainerListSpell.ReqAbility = trainerSpell.ReqAbility.ToArray();
|
||||
trainerListSpell.Usable = GetSpellState(player, trainerSpell);
|
||||
trainerListSpell.ReqLevel = trainerSpell.ReqLevel;
|
||||
trainerList.Spells.Add(trainerListSpell);
|
||||
}
|
||||
|
||||
player.SendPacket(trainerList);
|
||||
}
|
||||
|
||||
public void TeachSpell(Creature npc, Player player, uint spellId)
|
||||
{
|
||||
TrainerSpell trainerSpell = GetSpell(spellId);
|
||||
if (trainerSpell == null || !CanTeachSpell(player, trainerSpell))
|
||||
{
|
||||
SendTeachFailure(npc, player, spellId, TrainerFailReason.Unavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
float reputationDiscount = player.GetReputationPriceDiscount(npc);
|
||||
long moneyCost = (long)(trainerSpell.MoneyCost * reputationDiscount);
|
||||
if (!player.HasEnoughMoney(moneyCost))
|
||||
{
|
||||
SendTeachFailure(npc, player, spellId, TrainerFailReason.NotEnoughMoney);
|
||||
return;
|
||||
}
|
||||
|
||||
player.ModifyMoney(-moneyCost);
|
||||
|
||||
npc.SendPlaySpellVisualKit(179, 0, 0); // 53 SpellCastDirected
|
||||
player.SendPlaySpellVisualKit(362, 1, 0); // 113 EmoteSalute
|
||||
|
||||
// learn explicitly or cast explicitly
|
||||
if (trainerSpell.CastSpellId != 0)
|
||||
player.CastSpell(player, trainerSpell.CastSpellId, true);
|
||||
else
|
||||
player.LearnSpell(trainerSpell.SpellId, false);
|
||||
}
|
||||
|
||||
TrainerSpell GetSpell(uint spellId)
|
||||
{
|
||||
return _spells.Find(trainerSpell => trainerSpell.SpellId == spellId);
|
||||
}
|
||||
|
||||
bool CanTeachSpell(Player player, TrainerSpell trainerSpell)
|
||||
{
|
||||
TrainerSpellState state = GetSpellState(player, trainerSpell);
|
||||
if (state != TrainerSpellState.Available)
|
||||
return false;
|
||||
|
||||
SpellInfo trainerSpellInfo = Global.SpellMgr.GetSpellInfo(trainerSpell.SpellId);
|
||||
if (trainerSpellInfo.IsPrimaryProfessionFirstRank() && player.GetFreePrimaryProfessionPoints() == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TrainerSpellState GetSpellState(Player player, TrainerSpell trainerSpell)
|
||||
{
|
||||
if (player.HasSpell(trainerSpell.SpellId))
|
||||
return TrainerSpellState.Known;
|
||||
|
||||
// check race/class requirement
|
||||
if (!player.IsSpellFitByClassAndRace(trainerSpell.SpellId))
|
||||
return TrainerSpellState.Unavailable;
|
||||
|
||||
// check skill requirement
|
||||
if (trainerSpell.ReqSkillLine != 0 && player.GetBaseSkillValue((SkillType)trainerSpell.ReqSkillLine) < trainerSpell.ReqSkillRank)
|
||||
return TrainerSpellState.Unavailable;
|
||||
|
||||
foreach (uint reqAbility in trainerSpell.ReqAbility)
|
||||
if (reqAbility != 0 && !player.HasSpell(reqAbility))
|
||||
return TrainerSpellState.Unavailable;
|
||||
|
||||
// check level requirement
|
||||
if (player.getLevel() < trainerSpell.ReqLevel)
|
||||
return TrainerSpellState.Unavailable;
|
||||
|
||||
// check additional spell requirement
|
||||
foreach (var spellId in Global.SpellMgr.GetSpellsRequiredForSpellBounds(trainerSpell.SpellId))
|
||||
if (!player.HasSpell(spellId))
|
||||
return TrainerSpellState.Unavailable;
|
||||
|
||||
return TrainerSpellState.Available;
|
||||
}
|
||||
|
||||
void SendTeachFailure(Creature npc, Player player, uint spellId, TrainerFailReason reason)
|
||||
{
|
||||
TrainerBuyFailed trainerBuyFailed = new TrainerBuyFailed();
|
||||
trainerBuyFailed.TrainerGUID = npc.GetGUID();
|
||||
trainerBuyFailed.SpellID = spellId;
|
||||
trainerBuyFailed.TrainerFailedReason = reason;
|
||||
player.SendPacket(trainerBuyFailed);
|
||||
}
|
||||
|
||||
string GetGreeting(LocaleConstant locale)
|
||||
{
|
||||
if (_greeting[(int)locale].IsEmpty())
|
||||
return _greeting[(int)LocaleConstant.enUS];
|
||||
|
||||
return _greeting[(int)locale];
|
||||
}
|
||||
|
||||
public void AddGreetingLocale(LocaleConstant locale, string greeting)
|
||||
{
|
||||
_greeting[(int)locale] = greeting;
|
||||
}
|
||||
|
||||
uint _id;
|
||||
TrainerType _type;
|
||||
List<TrainerSpell> _spells;
|
||||
Array<string> _greeting = new Array<string>((int)LocaleConstant.Total);
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,8 @@ namespace Game.Entities
|
||||
|
||||
RestMgr _restMgr;
|
||||
|
||||
uint _currentTrainerId;
|
||||
|
||||
//Combat
|
||||
int[] baseRatingValue = new int[(int)CombatRating.Max];
|
||||
public float[][] m_auraBaseMod = new float[(int)BaseModGroup.End][];
|
||||
|
||||
@@ -1034,6 +1034,9 @@ namespace Game.Entities
|
||||
|
||||
public RestMgr GetRestMgr() { return _restMgr; }
|
||||
|
||||
public uint GetCurrentTrainerId() { return _currentTrainerId; }
|
||||
public void SetCurrentTrainerId(uint trainerId) { _currentTrainerId = trainerId; }
|
||||
|
||||
public bool IsAdvancedCombatLoggingEnabled() { return _advancedCombatLoggingEnabled; }
|
||||
public void SetAdvancedCombatLogging(bool enabled) { _advancedCombatLoggingEnabled = enabled; }
|
||||
|
||||
@@ -1093,26 +1096,6 @@ namespace Game.Entities
|
||||
|
||||
m_temporaryUnsummonedPetNumber = 0;
|
||||
}
|
||||
public void ResetPetTalents()
|
||||
{
|
||||
/* ODO: 6.x remove/update pet talents
|
||||
// This needs another gossip option + NPC text as a confirmation.
|
||||
// The confirmation gossip listid has the text: "Yes, please do."
|
||||
Pet pet = GetPet();
|
||||
|
||||
if (!pet || pet.getPetType() != PetType.Hunter || pet.m_usedTalentCount == 0)
|
||||
return;
|
||||
|
||||
CharmInfo charmInfo = pet.GetCharmInfo();
|
||||
if (charmInfo == null)
|
||||
{
|
||||
Log.outError(LogFilter.Player, "Object (GUID: {0} TypeId: {1}) is considered pet-like but doesn't have a charminfo!", pet.GetGUID().ToString(), pet.GetTypeId());
|
||||
return;
|
||||
}
|
||||
pet.resetTalents();
|
||||
SendTalentsInfoData(true);
|
||||
*/
|
||||
}
|
||||
|
||||
public bool IsPetNeedBeTemporaryUnsummoned()
|
||||
{
|
||||
@@ -2352,10 +2335,10 @@ namespace Game.Entities
|
||||
|
||||
foreach (var menuItems in menuItemBounds)
|
||||
{
|
||||
bool canTalk = true;
|
||||
if (!Global.ConditionMgr.IsObjectMeetToConditions(this, source, menuItems.Conditions))
|
||||
continue;
|
||||
|
||||
bool canTalk = true;
|
||||
GameObject go = source.ToGameObject();
|
||||
Creature creature = source.ToCreature();
|
||||
if (creature)
|
||||
@@ -2384,11 +2367,7 @@ namespace Game.Entities
|
||||
canTalk = false;
|
||||
break;
|
||||
case GossipOption.Unlearntalents:
|
||||
if (!creature.isCanTrainingAndResetTalentsOf(this))
|
||||
canTalk = false;
|
||||
break;
|
||||
case GossipOption.Unlearnpettalents:
|
||||
if (GetPet() == null || GetPet().getPetType() != PetType.Hunter || GetPet().m_spells.Count <= 1 || creature.GetCreatureTemplate().TrainerType != TrainerType.Pets || creature.GetCreatureTemplate().TrainerClass != Class.Hunter)
|
||||
if (!creature.CanResetTalents(this))
|
||||
canTalk = false;
|
||||
break;
|
||||
case GossipOption.Taxivendor:
|
||||
@@ -2407,11 +2386,6 @@ namespace Game.Entities
|
||||
canTalk = false;
|
||||
break;
|
||||
case GossipOption.Trainer:
|
||||
if (GetClass() != creature.GetCreatureTemplate().TrainerClass && creature.GetCreatureTemplate().TrainerType == TrainerType.Class)
|
||||
Log.outError(LogFilter.Sql, "GOSSIP_OPTION_TRAINER. Player {0} (GUID: {1}) request wrong gossip menu: {2} with wrong class: {3} at Creature: {4} (Entry: {5}, Trainer Class: {6})",
|
||||
GetName(), GetGUID().ToString(), menu.GetGossipMenu().GetMenuId(), GetClass(), creature.GetName(), creature.GetEntry(), creature.GetCreatureTemplate().TrainerClass);
|
||||
canTalk = false;
|
||||
break;
|
||||
case GossipOption.Gossip:
|
||||
case GossipOption.Spiritguide:
|
||||
case GossipOption.Innkeeper:
|
||||
@@ -2482,7 +2456,7 @@ namespace Game.Entities
|
||||
}
|
||||
|
||||
menu.GetGossipMenu().AddMenuItem((int)menuItems.OptionIndex, menuItems.OptionIcon, strOptionText, 0, (uint)menuItems.OptionType, strBoxText, menuItems.BoxMoney, menuItems.BoxCoded);
|
||||
menu.GetGossipMenu().AddGossipMenuItemData(menuItems.OptionIndex, menuItems.ActionMenuId, menuItems.ActionPoiId);
|
||||
menu.GetGossipMenu().AddGossipMenuItemData(menuItems.OptionIndex, menuItems.ActionMenuId, menuItems.ActionPoiId, menuItems.TrainerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2580,7 +2554,7 @@ namespace Game.Entities
|
||||
GetSession().SendStablePet(guid);
|
||||
break;
|
||||
case GossipOption.Trainer:
|
||||
GetSession().SendTrainerList(guid);
|
||||
GetSession().SendTrainerList(guid, menuItemData.TrainerId);
|
||||
break;
|
||||
case GossipOption.Learndualspec:
|
||||
break;
|
||||
@@ -2588,10 +2562,6 @@ namespace Game.Entities
|
||||
PlayerTalkClass.SendCloseGossip();
|
||||
SendRespecWipeConfirm(guid, GetNextResetTalentsCost());
|
||||
break;
|
||||
case GossipOption.Unlearnpettalents:
|
||||
PlayerTalkClass.SendCloseGossip();
|
||||
ResetPetTalents();
|
||||
break;
|
||||
case GossipOption.Taxivendor:
|
||||
GetSession().SendTaxiMenu(source.ToCreature());
|
||||
break;
|
||||
@@ -5484,76 +5454,6 @@ namespace Game.Entities
|
||||
return null;
|
||||
}
|
||||
|
||||
public TrainerSpellState GetTrainerSpellState(TrainerSpell trainer_spell)
|
||||
{
|
||||
if (trainer_spell == null)
|
||||
return TrainerSpellState.Red;
|
||||
|
||||
bool hasSpell = true;
|
||||
for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i)
|
||||
{
|
||||
if (trainer_spell.ReqAbility[i] == 0)
|
||||
continue;
|
||||
|
||||
if (!HasSpell(trainer_spell.ReqAbility[i]))
|
||||
{
|
||||
hasSpell = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// known spell
|
||||
if (hasSpell)
|
||||
return TrainerSpellState.Gray;
|
||||
|
||||
// check skill requirement
|
||||
if (trainer_spell.ReqSkillLine != 0 && GetBaseSkillValue((SkillType)trainer_spell.ReqSkillLine) < trainer_spell.ReqSkillRank)
|
||||
return TrainerSpellState.Red;
|
||||
|
||||
// check level requirement
|
||||
if (getLevel() < trainer_spell.ReqLevel)
|
||||
return TrainerSpellState.Red;
|
||||
|
||||
for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i)
|
||||
{
|
||||
if (trainer_spell.ReqAbility[i] == 0)
|
||||
continue;
|
||||
|
||||
// check race/class requirement
|
||||
if (!IsSpellFitByClassAndRace(trainer_spell.ReqAbility[i]))
|
||||
return TrainerSpellState.Red;
|
||||
|
||||
uint prevSpell = Global.SpellMgr.GetPrevSpellInChain(trainer_spell.ReqAbility[i]);
|
||||
if (prevSpell != 0)
|
||||
{
|
||||
// check prev.rank requirement
|
||||
if (!HasSpell(prevSpell))
|
||||
return TrainerSpellState.Red;
|
||||
}
|
||||
|
||||
var spellsRequired = Global.SpellMgr.GetSpellsRequiredForSpellBounds(trainer_spell.ReqAbility[i]);
|
||||
foreach (var spellId in spellsRequired)
|
||||
{
|
||||
// check additional spell requirement
|
||||
if (!HasSpell(spellId))
|
||||
return TrainerSpellState.Red;
|
||||
}
|
||||
}
|
||||
|
||||
// check primary prof. limit
|
||||
// first rank of primary profession spell when there are no proffesions avalible is disabled
|
||||
for (var i = 0; i < SharedConst.MaxTrainerspellAbilityReqs; ++i)
|
||||
{
|
||||
if (trainer_spell.ReqAbility[i] == 0)
|
||||
continue;
|
||||
|
||||
SpellInfo learnedSpellInfo = Global.SpellMgr.GetSpellInfo(trainer_spell.ReqAbility[i]);
|
||||
if (learnedSpellInfo != null && learnedSpellInfo.IsPrimaryProfessionFirstRank() && (GetFreePrimaryProfessionPoints() == 0))
|
||||
return TrainerSpellState.GreenDisabled;
|
||||
}
|
||||
|
||||
return TrainerSpellState.Green;
|
||||
}
|
||||
|
||||
public void SendInitialPacketsBeforeAddToMap()
|
||||
{
|
||||
if (!m_teleport_options.HasAnyFlag(TeleportToOptions.Seamless))
|
||||
|
||||
Reference in New Issue
Block a user