Core/Spells: Implement SPELL_EFFECT_ACTIVATE_OBJECT. (#23)
Original research by @xvwyh.
This commit is contained in:
1 parent
3910218a9b
commit
c69627f4b4
11 files changed
+208
-58
No files matched your search
@@ -93,6 +93,9 @@ static FactionTeamMap sFactionTeamMap;
|
||||
DBCStorage <FactionEntry> sFactionStore(FactionEntryfmt);
|
||||
DBCStorage <FactionTemplateEntry> sFactionTemplateStore(FactionTemplateEntryfmt);
|
||||
|
||||
// Used exclusively for data validation
|
||||
DBCStorage <GameObjectArtKitEntry> sGameObjectArtKitStore(GameObjectArtKitfmt);
|
||||
|
||||
DBCStorage <GameObjectDisplayInfoEntry> sGameObjectDisplayInfoStore(GameObjectDisplayInfofmt);
|
||||
DBCStorage <GemPropertiesEntry> sGemPropertiesStore(GemPropertiesEntryfmt);
|
||||
DBCStorage <GlyphPropertiesEntry> sGlyphPropertiesStore(GlyphPropertiesfmt);
|
||||
@@ -138,10 +141,10 @@ DBCStorage <ItemDamageEntry> sItemDamageTwoHandCasterStore(ItemDama
|
||||
DBCStorage <ItemDamageEntry> sItemDamageWandStore(ItemDamagefmt);
|
||||
DBCStorage <ItemDisenchantLootEntry> sItemDisenchantLootStore(ItemDisenchantLootfmt);
|
||||
//DBCStorage <ItemDisplayInfoEntry> sItemDisplayInfoStore(ItemDisplayTemplateEntryfmt); -- not used currently
|
||||
DBCStorage <ItemLimitCategoryEntry> sItemLimitCategoryStore(ItemLimitCategoryEntryfmt);
|
||||
DBCStorage <ItemRandomPropertiesEntry> sItemRandomPropertiesStore(ItemRandomPropertiesfmt);
|
||||
DBCStorage <ItemRandomSuffixEntry> sItemRandomSuffixStore(ItemRandomSuffixfmt);
|
||||
DBCStorage <ItemSetEntry> sItemSetStore(ItemSetEntryfmt);
|
||||
DBCStorage <ItemLimitCategoryEntry> sItemLimitCategoryStore(ItemLimitCategoryEntryfmt);
|
||||
DBCStorage <ItemRandomPropertiesEntry> sItemRandomPropertiesStore(ItemRandomPropertiesfmt);
|
||||
DBCStorage <ItemRandomSuffixEntry> sItemRandomSuffixStore(ItemRandomSuffixfmt);
|
||||
DBCStorage <ItemSetEntry> sItemSetStore(ItemSetEntryfmt);
|
||||
|
||||
DBCStorage <LFGDungeonEntry> sLFGDungeonStore(LFGDungeonEntryfmt);
|
||||
DBCStorage <LFGDungeonsGroupingMapEntry> sLFGDungeonsGroupingMapStore(LFGDungeonsGroupingMapfmt);
|
||||
@@ -374,6 +377,7 @@ void LoadDBCStores(const std::string& dataPath)
|
||||
LOAD_DBC(sEmotesTextStore, "EmotesText.dbc");//15595
|
||||
LOAD_DBC(sFactionStore, "Faction.dbc");//15595
|
||||
LOAD_DBC(sFactionTemplateStore, "FactionTemplate.dbc");//15595
|
||||
LOAD_DBC(sGameObjectArtKitStore, "GameObjectArtKit.dbc");//15595
|
||||
LOAD_DBC(sGameObjectDisplayInfoStore, "GameObjectDisplayInfo.dbc");//15595
|
||||
LOAD_DBC(sGemPropertiesStore, "GemProperties.dbc");//15595
|
||||
LOAD_DBC(sGlyphPropertiesStore, "GlyphProperties.dbc");//15595
|
||||
|
||||
@@ -129,6 +129,7 @@ TC_GAME_API extern DBCStorage <EmotesTextEntry> sEmotesTextStore;
|
||||
TC_GAME_API extern DBCStorage <EmotesTextSoundEntry> sEmotesTextSoundStore;
|
||||
TC_GAME_API extern DBCStorage <FactionEntry> sFactionStore;
|
||||
TC_GAME_API extern DBCStorage <FactionTemplateEntry> sFactionTemplateStore;
|
||||
TC_GAME_API extern DBCStorage <GameObjectArtKitEntry> sGameObjectArtKitStore;
|
||||
TC_GAME_API extern DBCStorage <GameObjectDisplayInfoEntry> sGameObjectDisplayInfoStore;
|
||||
TC_GAME_API extern DBCStorage <GemPropertiesEntry> sGemPropertiesStore;
|
||||
TC_GAME_API extern DBCStorage <GlyphPropertiesEntry> sGlyphPropertiesStore;
|
||||
|
||||
@@ -1195,6 +1195,13 @@ struct FactionTemplateEntry
|
||||
bool IsContestedGuardFaction() const { return (factionFlags & FACTION_TEMPLATE_FLAG_CONTESTED_GUARD) != 0; }
|
||||
};
|
||||
|
||||
struct GameObjectArtKitEntry
|
||||
{
|
||||
uint32 ID; // 0
|
||||
//char* TextureVariation[3] // 1-3 m_textureVariations[3]
|
||||
//char* AttachModel[4] // 4-8 m_attachModels[4]
|
||||
};
|
||||
|
||||
struct GameObjectDisplayInfoEntry
|
||||
{
|
||||
uint32 Displayid; // 0 m_ID
|
||||
|
||||
@@ -62,6 +62,7 @@ char const EmotesTextEntryfmt[] = "nxixxxxxxxxxxxxxxxx";
|
||||
char const EmotesTextSoundEntryfmt[] = "niiii";
|
||||
char const FactionEntryfmt[] = "niiiiiiiiiiiiiiiiiiffixsxi";
|
||||
char const FactionTemplateEntryfmt[] = "niiiiiiiiiiiii";
|
||||
char const GameObjectArtKitfmt[] = "nxxxxxxx";
|
||||
char const GameObjectDisplayInfofmt[] = "nsxxxxxxxxxxffffffxxx";
|
||||
char const GemPropertiesEntryfmt[] = "nixxii";
|
||||
char const GlyphPropertiesfmt[] = "niii";
|
||||
|
||||
@@ -1277,6 +1277,32 @@ void GameObject::UseDoorOrButton(uint32 time_to_restore, bool alternative /* = f
|
||||
m_cooldownTime = time_to_restore ? (GameTime::GetGameTimeMS() + time_to_restore) : 0;
|
||||
}
|
||||
|
||||
void GameObject::PlayAnimKit(int32 animKit)
|
||||
{
|
||||
WorldPacket data(SMSG_GAME_OBJECT_ACTIVATE_ANIM_KIT, 4 + 2);
|
||||
|
||||
ObjectGuid guid = GetGUID();
|
||||
data.WriteBit(guid[5]);
|
||||
data.WriteBit(guid[1]);
|
||||
data.WriteBit(guid[0]);
|
||||
data.WriteBit(guid[4]);
|
||||
data.WriteBit(guid[7]);
|
||||
data.WriteBit(guid[2]);
|
||||
data.WriteBit(guid[3]);
|
||||
data.WriteBit(guid[6]);
|
||||
data.WriteByteSeq(guid[5]);
|
||||
data.WriteByteSeq(guid[1]);
|
||||
data.WriteByteSeq(guid[0]);
|
||||
data.WriteByteSeq(guid[3]);
|
||||
data.WriteByteSeq(guid[4]);
|
||||
data.WriteByteSeq(guid[6]);
|
||||
data.WriteByteSeq(guid[2]);
|
||||
data.WriteByteSeq(guid[7]);
|
||||
data << int32(animKit);
|
||||
|
||||
SendMessageToSet(&data, nullptr);
|
||||
}
|
||||
|
||||
void GameObject::SetGoArtKit(uint8 kit)
|
||||
{
|
||||
SetByteValue(GAMEOBJECT_BYTES_1, 2, kit);
|
||||
|
||||
@@ -169,6 +169,8 @@ class TC_GAME_API GameObject : public WorldObject, public GridObject<GameObject>
|
||||
void SetGoAnimProgress(uint8 animprogress) { SetByteValue(GAMEOBJECT_BYTES_1, 3, animprogress); }
|
||||
static void SetGoArtKit(uint8 artkit, GameObject* go, ObjectGuid::LowType lowguid = 0);
|
||||
|
||||
void PlayAnimKit(int32 animKit);
|
||||
|
||||
void EnableCollision(bool enable);
|
||||
|
||||
void Use(Unit* user);
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
#include "SharedDefines.h"
|
||||
#include "SpawnData.h"
|
||||
#include "WorldPacket.h"
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
#define MAX_GAMEOBJECT_QUEST_ITEMS 6
|
||||
|
||||
// from `gameobject_template`
|
||||
@@ -597,6 +598,7 @@ struct GameObjectTemplateAddon
|
||||
uint32 flags;
|
||||
uint32 mingold;
|
||||
uint32 maxgold;
|
||||
std::array<uint32, 5> artKits;
|
||||
};
|
||||
|
||||
struct GameObjectLocale
|
||||
@@ -634,4 +636,47 @@ struct GameObjectData : public SpawnData
|
||||
uint8 artKit = 0;
|
||||
};
|
||||
|
||||
enum class GameObjectActions : uint32
|
||||
{
|
||||
// Name from client executable // Comments
|
||||
None, // -NONE-
|
||||
AnimateCustom0, // Animate Custom0
|
||||
AnimateCustom1, // Animate Custom1
|
||||
AnimateCustom2, // Animate Custom2
|
||||
AnimateCustom3, // Animate Custom3
|
||||
Disturb, // Disturb // Triggers trap
|
||||
Unlock, // Unlock // Resets GO_FLAG_LOCKED
|
||||
Lock, // Lock // Sets GO_FLAG_LOCKED
|
||||
Open, // Open // Sets GO_STATE_ACTIVE
|
||||
OpenAndUnlock, // Open + Unlock // Sets GO_STATE_ACTIVE and resets GO_FLAG_LOCKED
|
||||
Close, // Close // Sets GO_STATE_READY
|
||||
ToggleOpen, // Toggle Open
|
||||
Destroy, // Destroy // Sets GO_STATE_DESTROYED
|
||||
Rebuild, // Rebuild // Resets from GO_STATE_DESTROYED
|
||||
Creation, // Creation
|
||||
Despawn, // Despawn
|
||||
MakeInert, // Make Inert // Disables interactions
|
||||
MakeActive, // Make Active // Enables interactions
|
||||
CloseAndLock, // Close + Lock // Sets GO_STATE_READY and sets GO_FLAG_LOCKED
|
||||
UseArtKit0, // Use ArtKit0 // 46904: 121
|
||||
UseArtKit1, // Use ArtKit1 // 36639: 81, 46903: 122
|
||||
UseArtKit2, // Use ArtKit2
|
||||
UseArtKit3, // Use ArtKit3
|
||||
SetTapList, // Set Tap List
|
||||
GoTo1stFloor, // Go to 1st floor
|
||||
GoTo2ndFloor, // Go to 2nd floor
|
||||
GoTo3rdFloor, // Go to 3rd floor
|
||||
GoTo4thFloor, // Go to 4th floor
|
||||
GoTo5thFloor, // Go to 5th floor
|
||||
GoTo6thFloor, // Go to 6th floor
|
||||
GoTo7thFloor, // Go to 7th floor
|
||||
GoTo8thFloor, // Go to 8th floor
|
||||
GoTo9thFloor, // Go to 9th floor
|
||||
GoTo10thFloor, // Go to 10th floor
|
||||
UseArtKit4, // Use ArtKit4
|
||||
PlayAnimKit, // Play Anim Kit "%s" // MiscValueB -> Anim Kit ID
|
||||
OpenAndPlayAnimKit, // Open + Play Anim Kit "%s" // MiscValueB -> Anim Kit ID
|
||||
CloseAndPlayAnimKit, // Close + Play Anim Kit "%s" // MiscValueB -> Anim Kit ID
|
||||
};
|
||||
|
||||
#endif // GameObjectData_h__
|
||||
@@ -7488,8 +7488,8 @@ void ObjectMgr::LoadGameObjectTemplateAddons()
|
||||
{
|
||||
uint32 oldMSTime = getMSTime();
|
||||
|
||||
// 0 1 2 3 4
|
||||
QueryResult result = WorldDatabase.Query("SELECT entry, faction, flags, mingold, maxgold FROM gameobject_template_addon");
|
||||
// 0 1 2 3 4 5 6 7 8 9
|
||||
QueryResult result = WorldDatabase.Query("SELECT entry, faction, flags, mingold, maxgold, artkit0, artkit1, artkit2, artkit3, artkit4 FROM gameobject_template_addon");
|
||||
|
||||
if (!result)
|
||||
{
|
||||
@@ -7517,6 +7517,9 @@ void ObjectMgr::LoadGameObjectTemplateAddons()
|
||||
gameObjectAddon.mingold = fields[3].GetUInt32();
|
||||
gameObjectAddon.maxgold = fields[4].GetUInt32();
|
||||
|
||||
for (uint32 i = 0; i < gameObjectAddon.artKits.size(); ++i)
|
||||
gameObjectAddon.artKits[i] = fields[5 + i].GetUInt32();
|
||||
|
||||
// checks
|
||||
if (gameObjectAddon.faction && !sFactionTemplateStore.LookupEntry(gameObjectAddon.faction))
|
||||
TC_LOG_ERROR("sql.sql", "GameObject (Entry: %u) has invalid faction (%u) defined in `gameobject_template_addon`.", entry, gameObjectAddon.faction);
|
||||
@@ -7534,6 +7537,20 @@ void ObjectMgr::LoadGameObjectTemplateAddons()
|
||||
}
|
||||
}
|
||||
|
||||
uint32 artKitIndex = 0;
|
||||
for (uint32& artKitID : gameObjectAddon.artKits)
|
||||
{
|
||||
++artKitIndex;
|
||||
if (!artKitID)
|
||||
continue;
|
||||
|
||||
if (!sGameObjectArtKitStore.LookupEntry(artKitID))
|
||||
{
|
||||
TC_LOG_ERROR("sql.sql", "GameObject (Entry: %u) has invalid `artkit%d` (%d) defined, set to zero instead.", entry, artKitIndex - 1, artKitID);
|
||||
artKitID = 0;
|
||||
}
|
||||
}
|
||||
|
||||
++count;
|
||||
}
|
||||
while (result->NextRow());
|
||||
|
||||
@@ -3729,7 +3729,7 @@ void Spell::EffectSummonPlayer(SpellEffIndex /*effIndex*/)
|
||||
unitTarget->ToPlayer()->SendSummonRequestFrom(m_caster);
|
||||
}
|
||||
|
||||
void Spell::EffectActivateObject(SpellEffIndex /*effIndex*/)
|
||||
void Spell::EffectActivateObject(SpellEffIndex effIndex)
|
||||
{
|
||||
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)
|
||||
return;
|
||||
@@ -3737,12 +3737,105 @@ void Spell::EffectActivateObject(SpellEffIndex /*effIndex*/)
|
||||
if (!gameObjTarget)
|
||||
return;
|
||||
|
||||
ScriptInfo activateCommand;
|
||||
activateCommand.command = SCRIPT_COMMAND_ACTIVATE_OBJECT;
|
||||
GameObjectActions action = GameObjectActions(m_spellInfo->Effects[effIndex].MiscValue);
|
||||
|
||||
// int32 unk = m_spellInfo->Effects[effIndex].MiscValue; // This is set for EffectActivateObject spells; needs research
|
||||
switch (action)
|
||||
{
|
||||
case GameObjectActions::AnimateCustom0:
|
||||
case GameObjectActions::AnimateCustom1:
|
||||
case GameObjectActions::AnimateCustom2:
|
||||
case GameObjectActions::AnimateCustom3:
|
||||
gameObjTarget->SendCustomAnim(uint32(action) - uint32(GameObjectActions::AnimateCustom0));
|
||||
break;
|
||||
case GameObjectActions::Disturb: // What's the difference with Open?
|
||||
case GameObjectActions::Open:
|
||||
gameObjTarget->Use(m_caster);
|
||||
break;
|
||||
case GameObjectActions::OpenAndUnlock:
|
||||
gameObjTarget->UseDoorOrButton(0, false, m_caster);
|
||||
// no break
|
||||
case GameObjectActions::Unlock:
|
||||
case GameObjectActions::Lock:
|
||||
gameObjTarget->ApplyModFlag(GAMEOBJECT_FLAGS, GO_FLAG_LOCKED, action == GameObjectActions::Lock);
|
||||
break;
|
||||
case GameObjectActions::Close:
|
||||
case GameObjectActions::Rebuild:
|
||||
gameObjTarget->ResetDoorOrButton();
|
||||
break;
|
||||
case GameObjectActions::Despawn:
|
||||
gameObjTarget->DespawnOrUnsummon();
|
||||
break;
|
||||
case GameObjectActions::MakeInert:
|
||||
case GameObjectActions::MakeActive:
|
||||
gameObjTarget->ApplyModFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE, action == GameObjectActions::MakeInert);
|
||||
break;
|
||||
case GameObjectActions::CloseAndLock:
|
||||
gameObjTarget->ResetDoorOrButton();
|
||||
gameObjTarget->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_LOCKED);
|
||||
break;
|
||||
case GameObjectActions::Destroy:
|
||||
gameObjTarget->UseDoorOrButton(0, true, m_caster);
|
||||
break;
|
||||
case GameObjectActions::UseArtKit0:
|
||||
case GameObjectActions::UseArtKit1:
|
||||
case GameObjectActions::UseArtKit2:
|
||||
case GameObjectActions::UseArtKit3:
|
||||
case GameObjectActions::UseArtKit4:
|
||||
{
|
||||
GameObjectTemplateAddon const* templateAddon = gameObjTarget->GetTemplateAddon();
|
||||
|
||||
uint32 artKitIndex = uint32(action) - uint32(GameObjectActions::UseArtKit0);
|
||||
// Non-sequential here
|
||||
if (action == GameObjectActions::UseArtKit4)
|
||||
artKitIndex = 4;
|
||||
|
||||
uint32 artKitValue = 0;
|
||||
if (templateAddon != nullptr)
|
||||
artKitValue = templateAddon->artKits[artKitIndex];
|
||||
|
||||
if (artKitValue == 0)
|
||||
TC_LOG_ERROR("sql.sql", "GameObject %d hit by spell %d needs `artkit%d` in `gameobject_template_addon`", gameObjTarget->GetEntry(), m_spellInfo->Id, artKitIndex);
|
||||
else
|
||||
gameObjTarget->SetGoArtKit(artKitValue);
|
||||
|
||||
break;
|
||||
}
|
||||
case GameObjectActions::PlayAnimKit:
|
||||
gameObjTarget->PlayAnimKit(m_spellInfo->Effects[effIndex].MiscValueB);
|
||||
break;
|
||||
break;
|
||||
case GameObjectActions::OpenAndPlayAnimKit:
|
||||
gameObjTarget->Use(m_caster);
|
||||
gameObjTarget->PlayAnimKit(m_spellInfo->Effects[effIndex].MiscValueB);
|
||||
break;
|
||||
case GameObjectActions::CloseAndPlayAnimKit:
|
||||
gameObjTarget->ResetDoorOrButton();
|
||||
gameObjTarget->PlayAnimKit(m_spellInfo->Effects[effIndex].MiscValueB);
|
||||
break;
|
||||
case GameObjectActions::GoTo1stFloor:
|
||||
case GameObjectActions::GoTo2ndFloor:
|
||||
case GameObjectActions::GoTo3rdFloor:
|
||||
case GameObjectActions::GoTo4thFloor:
|
||||
case GameObjectActions::GoTo5thFloor:
|
||||
case GameObjectActions::GoTo6thFloor:
|
||||
case GameObjectActions::GoTo7thFloor:
|
||||
case GameObjectActions::GoTo8thFloor:
|
||||
case GameObjectActions::GoTo9thFloor:
|
||||
case GameObjectActions::GoTo10thFloor:
|
||||
{
|
||||
// Cast is kind of loose but it'll do
|
||||
if (Transport* transportTarget = gameObjTarget->ToTransport())
|
||||
transportTarget->SetTransportState(GO_STATE_TRANSPORT_STOPPED, uint32_t(action) - uint32(GameObjectActions::GoTo1stFloor));
|
||||
break;
|
||||
}
|
||||
case GameObjectActions::None:
|
||||
TC_LOG_FATAL("spell", "Spell %d has action type NONE in effect %d", m_spellInfo->Id, int32(effIndex));
|
||||
break;
|
||||
default:
|
||||
TC_LOG_ERROR("spell", "Spell %d has unhandled action %d in effect %d", m_spellInfo->Id, int32(action), int32(effIndex));
|
||||
break;
|
||||
}
|
||||
|
||||
gameObjTarget->GetMap()->ScriptCommandStart(activateCommand, 0, m_caster, gameObjTarget);
|
||||
}
|
||||
|
||||
void Spell::EffectApplyGlyph(SpellEffIndex effIndex)
|
||||
|
||||
@@ -469,26 +469,8 @@ private:
|
||||
bool _firstGuardian;
|
||||
};
|
||||
|
||||
// 45226 - Banging the Gong
|
||||
class spell_banging_the_gong : public SpellScript
|
||||
{
|
||||
PrepareSpellScript(spell_banging_the_gong);
|
||||
|
||||
void Activate(SpellEffIndex effIndex)
|
||||
{
|
||||
PreventHitDefaultEffect(effIndex);
|
||||
GetHitGObj()->SendCustomAnim(0);
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
OnEffectHitTarget += SpellEffectFn(spell_banging_the_gong::Activate, EFFECT_1, SPELL_EFFECT_ACTIVATE_OBJECT);
|
||||
}
|
||||
};
|
||||
|
||||
void AddSC_zulaman()
|
||||
{
|
||||
RegisterZulAamanCreatureAI(npc_zulaman_voljin);
|
||||
RegisterZulAamanCreatureAI(npc_zulaman_amanishi_guardian);
|
||||
RegisterSpellScript(spell_banging_the_gong);
|
||||
}
|
||||
@@ -422,33 +422,6 @@ class spell_anhuur_disable_beacon_beams : public SpellScript
|
||||
}
|
||||
};
|
||||
|
||||
// 76599 - Activate Beacons
|
||||
// 76600 - Deactivate Beacons
|
||||
class spell_anhuur_handle_beacons : public SpellScript
|
||||
{
|
||||
PrepareSpellScript(spell_anhuur_handle_beacons);
|
||||
|
||||
void HandleEffect(SpellEffIndex effIndex)
|
||||
{
|
||||
PreventHitDefaultEffect(effIndex);
|
||||
int32 script = GetSpellInfo()->Effects[effIndex].MiscValue;
|
||||
|
||||
GameObject* beacon = GetHitGObj();
|
||||
if (!beacon)
|
||||
return;
|
||||
|
||||
if (script == 16) // 16 => Disable
|
||||
beacon->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_IN_USE);
|
||||
else if (script == 17) // 17 => Activate
|
||||
beacon->RemoveFlag(GAMEOBJECT_FLAGS, GO_FLAG_IN_USE);
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
OnEffectHitTarget += SpellEffectFn(spell_anhuur_handle_beacons::HandleEffect, EFFECT_0, SPELL_EFFECT_ACTIVATE_OBJECT);
|
||||
}
|
||||
};
|
||||
|
||||
class spell_anhuur_burning_light_forcecast : public SpellScript
|
||||
{
|
||||
PrepareSpellScript(spell_anhuur_burning_light_forcecast);
|
||||
@@ -493,7 +466,6 @@ void AddSC_boss_temple_guardian_anhuur()
|
||||
RegisterSpellScript(spell_anhuur_shield_of_light),
|
||||
RegisterAuraScript(spell_anhuur_reverberating_hymn);
|
||||
RegisterSpellScript(spell_anhuur_disable_beacon_beams);
|
||||
RegisterSpellScript(spell_anhuur_handle_beacons);
|
||||
RegisterSpellScript(spell_anhuur_burning_light_forcecast);
|
||||
new achievement_i_hate_that_song();
|
||||
}
|
||||
Reference in new issue
Block a user