forked from liza/Questionable
Add quest battle notes
This commit is contained in:
parent
a75286e927
commit
31eb121cf0
@ -126,6 +126,9 @@ internal static class QuestStepExtensions
|
|||||||
Assignment(nameof(QuestStep.BossModEnabled),
|
Assignment(nameof(QuestStep.BossModEnabled),
|
||||||
step.BossModEnabled, emptyStep.BossModEnabled)
|
step.BossModEnabled, emptyStep.BossModEnabled)
|
||||||
.AsSyntaxNodeOrToken(),
|
.AsSyntaxNodeOrToken(),
|
||||||
|
Assignment(nameof(QuestStep.BossModNotes),
|
||||||
|
step.BossModNotes, emptyStep.BossModNotes)
|
||||||
|
.AsSyntaxNodeOrToken(),
|
||||||
Assignment(nameof(QuestStep.SinglePlayerDutyIndex),
|
Assignment(nameof(QuestStep.SinglePlayerDutyIndex),
|
||||||
step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex)
|
step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex)
|
||||||
.AsSyntaxNodeOrToken(),
|
.AsSyntaxNodeOrToken(),
|
||||||
|
@ -103,7 +103,11 @@
|
|||||||
"Z": 479.9724
|
"Z": 479.9724
|
||||||
},
|
},
|
||||||
"TerritoryId": 1053,
|
"TerritoryId": 1053,
|
||||||
"InteractionType": "SinglePlayerDuty"
|
"InteractionType": "SinglePlayerDuty",
|
||||||
|
"BossModEnabled": false,
|
||||||
|
"BossModNotes": [
|
||||||
|
"Doesn't handle death properly"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -61,7 +61,19 @@
|
|||||||
"TerritoryId": 156,
|
"TerritoryId": 156,
|
||||||
"InteractionType": "Interact",
|
"InteractionType": "Interact",
|
||||||
"AetheryteShortcut": "Mor Dhona",
|
"AetheryteShortcut": "Mor Dhona",
|
||||||
"TargetTerritoryId": 351
|
"TargetTerritoryId": 351,
|
||||||
|
"SkipConditions": {
|
||||||
|
"AetheryteShortcutIf": {
|
||||||
|
"InTerritory": [
|
||||||
|
351
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"StepIf": {
|
||||||
|
"InTerritory": [
|
||||||
|
351
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"DataId": 1032081,
|
"DataId": 1032081,
|
||||||
@ -73,13 +85,14 @@
|
|||||||
"TerritoryId": 351,
|
"TerritoryId": 351,
|
||||||
"InteractionType": "SinglePlayerDuty",
|
"InteractionType": "SinglePlayerDuty",
|
||||||
"Comment": "Estinien vs. Arch Ultima",
|
"Comment": "Estinien vs. Arch Ultima",
|
||||||
"DialogueChoices": [
|
"BossModEnabled": false,
|
||||||
{
|
"BossModNotes": [
|
||||||
"Type": "YesNo",
|
"AI doesn't move automatically for the first boss",
|
||||||
"Prompt": "TEXT_LUCKMG110_03682_Q1_100_125",
|
"AI doesn't move automatically for the dialogue with gaius on the bridge",
|
||||||
"Yes": true
|
"After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)",
|
||||||
}
|
"After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking"
|
||||||
]
|
],
|
||||||
|
"$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1270,6 +1270,12 @@
|
|||||||
"BossModEnabled": {
|
"BossModEnabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"BossModNotes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"SinglePlayerDutyIndex": {
|
"SinglePlayerDutyIndex": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
|
@ -76,6 +76,7 @@ public sealed class QuestStep
|
|||||||
public uint? ContentFinderConditionId { get; set; }
|
public uint? ContentFinderConditionId { get; set; }
|
||||||
public bool AutoDutyEnabled { get; set; }
|
public bool AutoDutyEnabled { get; set; }
|
||||||
public bool BossModEnabled { get; set; }
|
public bool BossModEnabled { get; set; }
|
||||||
|
public List<string> BossModNotes { get; set; } = [];
|
||||||
public byte SinglePlayerDutyIndex { get; set; }
|
public byte SinglePlayerDutyIndex { get; set; }
|
||||||
public SkipConditions? SkipConditions { get; set; }
|
public SkipConditions? SkipConditions { get; set; }
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ using Lumina.Excel.Sheets;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Questionable.Controller.Steps.Interactions;
|
using Questionable.Controller.Steps.Interactions;
|
||||||
using Questionable.Data;
|
using Questionable.Data;
|
||||||
|
using Questionable.External;
|
||||||
using Questionable.Functions;
|
using Questionable.Functions;
|
||||||
using Questionable.Model;
|
using Questionable.Model;
|
||||||
using Questionable.Model.Gathering;
|
using Questionable.Model.Gathering;
|
||||||
@ -45,6 +46,7 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
private readonly ITargetManager _targetManager;
|
private readonly ITargetManager _targetManager;
|
||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly ShopController _shopController;
|
private readonly ShopController _shopController;
|
||||||
|
private readonly BossModIpc _bossModIpc;
|
||||||
private readonly ILogger<InteractionUiController> _logger;
|
private readonly ILogger<InteractionUiController> _logger;
|
||||||
private readonly Regex _returnRegex;
|
private readonly Regex _returnRegex;
|
||||||
private readonly Regex _purchaseItemRegex;
|
private readonly Regex _purchaseItemRegex;
|
||||||
@ -68,6 +70,7 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
IPluginLog pluginLog,
|
IPluginLog pluginLog,
|
||||||
IClientState clientState,
|
IClientState clientState,
|
||||||
ShopController shopController,
|
ShopController shopController,
|
||||||
|
BossModIpc bossModIpc,
|
||||||
ILogger<InteractionUiController> logger)
|
ILogger<InteractionUiController> logger)
|
||||||
{
|
{
|
||||||
_addonLifecycle = addonLifecycle;
|
_addonLifecycle = addonLifecycle;
|
||||||
@ -85,6 +88,7 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
_targetManager = targetManager;
|
_targetManager = targetManager;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_shopController = shopController;
|
_shopController = shopController;
|
||||||
|
_bossModIpc = bossModIpc;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!;
|
_returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!;
|
||||||
@ -176,7 +180,10 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
|
|
||||||
int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
|
int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
|
||||||
if (answer != null)
|
if (answer != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt);
|
||||||
addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
|
addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
|
private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
|
||||||
@ -224,6 +231,7 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
|
int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
|
||||||
if (answer != null)
|
if (answer != null)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt);
|
||||||
addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
|
addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -266,6 +274,7 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
|
int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
|
||||||
if (questSelection >= 0)
|
if (questSelection >= 0)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Selecting quest {QuestName}", questName);
|
||||||
addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
|
addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -655,13 +664,21 @@ internal sealed class InteractionUiController : IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Returning {YesNo} for '{Prompt}'", dialogueChoice.Yes ? "Yes" : "No", actualPrompt);
|
||||||
addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
|
addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true })
|
if (step is { InteractionType: EInteractionType.SinglePlayerDuty } &&
|
||||||
|
_bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled))
|
||||||
{
|
{
|
||||||
_logger.LogTrace("DefaultYesNo: probably Single Player Duty");
|
// Most of these are yes/no dialogs "Duty calls, ...".
|
||||||
|
//
|
||||||
|
// For 'Vows of Virtue, Deeds of Cruelty', there's no such dialog, and it just puts you into the instance
|
||||||
|
// after you confirm 'Wait for Krile?'. However, if you fail that duty, you'll get a DifficultySelectYesNo.
|
||||||
|
|
||||||
|
// DifficultySelectYesNo → [0, 2] for very easy
|
||||||
|
_logger.LogInformation("DefaultYesNo: probably Single Player Duty");
|
||||||
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
|
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
3
Questionable/External/BossModIpc.cs
vendored
3
Questionable/External/BossModIpc.cs
vendored
@ -84,6 +84,9 @@ internal sealed class BossModIpc
|
|||||||
|
|
||||||
public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
|
public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
|
||||||
{
|
{
|
||||||
|
if (!IsSupported())
|
||||||
|
return false;
|
||||||
|
|
||||||
if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
|
if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -40,12 +40,22 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
(EClassJob.BlackMage, "Magical Ranged Role Quests"),
|
(EClassJob.BlackMage, "Magical Ranged Role Quests"),
|
||||||
];
|
];
|
||||||
|
|
||||||
private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles = ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty;
|
private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles =
|
||||||
private ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>> _mainScenarioBattles = ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>>.Empty;
|
ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty;
|
||||||
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _jobQuestBattles = ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
|
|
||||||
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _roleQuestBattles = ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
|
private ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>> _mainScenarioBattles =
|
||||||
|
ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>>.Empty;
|
||||||
|
|
||||||
|
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _jobQuestBattles =
|
||||||
|
ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
|
||||||
|
|
||||||
|
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _roleQuestBattles =
|
||||||
|
ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
|
||||||
|
|
||||||
private ImmutableList<SinglePlayerDutyInfo> _otherRoleQuestBattles = ImmutableList<SinglePlayerDutyInfo>.Empty;
|
private ImmutableList<SinglePlayerDutyInfo> _otherRoleQuestBattles = ImmutableList<SinglePlayerDutyInfo>.Empty;
|
||||||
private ImmutableList<(string Label, List<SinglePlayerDutyInfo>)> _otherQuestBattles = ImmutableList<(string Label, List<SinglePlayerDutyInfo>)>.Empty;
|
|
||||||
|
private ImmutableList<(string Label, List<SinglePlayerDutyInfo>)> _otherQuestBattles =
|
||||||
|
ImmutableList<(string Label, List<SinglePlayerDutyInfo>)>.Empty;
|
||||||
|
|
||||||
public SinglePlayerDutyConfigComponent(
|
public SinglePlayerDutyConfigComponent(
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
@ -103,10 +113,10 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
{
|
{
|
||||||
IQuestInfo questInfo = _questData.GetQuestInfo(questId);
|
IQuestInfo questInfo = _questData.GetQuestInfo(questId);
|
||||||
QuestStep questStep = new QuestStep
|
QuestStep questStep = new QuestStep
|
||||||
{
|
{
|
||||||
SinglePlayerDutyIndex = 0,
|
SinglePlayerDutyIndex = 0,
|
||||||
BossModEnabled = false,
|
BossModEnabled = false,
|
||||||
};
|
};
|
||||||
bool enabled;
|
bool enabled;
|
||||||
if (_questRegistry.TryGetQuest(questId, out var quest))
|
if (_questRegistry.TryGetQuest(questId, out var quest))
|
||||||
{
|
{
|
||||||
@ -122,7 +132,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
x.Step.SinglePlayerDutyIndex == index);
|
x.Step.SinglePlayerDutyIndex == index);
|
||||||
if (foundStep == default)
|
if (foundStep == default)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, index);
|
_logger.LogWarning(
|
||||||
|
"Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId,
|
||||||
|
index);
|
||||||
enabled = false;
|
enabled = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -156,7 +168,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
questInfo.SortKey,
|
questInfo.SortKey,
|
||||||
questStep.SinglePlayerDutyIndex,
|
questStep.SinglePlayerDutyIndex,
|
||||||
enabled,
|
enabled,
|
||||||
questStep.BossModEnabled);
|
questStep.BossModEnabled,
|
||||||
|
questStep.BossModNotes);
|
||||||
|
|
||||||
if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334)
|
if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334)
|
||||||
startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo);
|
startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo);
|
||||||
@ -343,7 +356,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ImGui.CollapsingHeader("General Role Quests"))
|
if (ImGui.CollapsingHeader("General Role Quests"))
|
||||||
DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles);
|
DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,9 +393,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
? SupportedCfcOptions
|
? SupportedCfcOptions
|
||||||
: UnsupportedCfcOptions;
|
: UnsupportedCfcOptions;
|
||||||
int value = 0;
|
int value = 0;
|
||||||
if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(dutyInfo.CfcId))
|
if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
|
||||||
value = 1;
|
value = 1;
|
||||||
if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(dutyInfo.CfcId))
|
if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
|
||||||
value = 2;
|
value = 2;
|
||||||
|
|
||||||
if (ImGui.TableNextColumn())
|
if (ImGui.TableNextColumn())
|
||||||
@ -407,6 +420,25 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.",
|
ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.",
|
||||||
FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
|
FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
|
||||||
}
|
}
|
||||||
|
else if (dutyInfo.Notes.Count > 0)
|
||||||
|
{
|
||||||
|
using var color = new ImRaii.Color();
|
||||||
|
color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow);
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
using var _ = ImRaii.Tooltip();
|
||||||
|
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudYellow, "While testing, the following issues have been found:");
|
||||||
|
foreach (string note in dutyInfo.Notes)
|
||||||
|
ImGui.BulletText(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.TableNextColumn())
|
if (ImGui.TableNextColumn())
|
||||||
@ -417,13 +449,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
ImGui.SetNextItemWidth(200);
|
ImGui.SetNextItemWidth(200);
|
||||||
if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
|
if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
|
||||||
{
|
{
|
||||||
Configuration.Duties.WhitelistedDutyCfcIds.Remove(dutyInfo.CfcId);
|
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
|
||||||
Configuration.Duties.BlacklistedDutyCfcIds.Remove(dutyInfo.CfcId);
|
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
|
||||||
|
|
||||||
if (value == 1)
|
if (value == 1)
|
||||||
Configuration.Duties.WhitelistedDutyCfcIds.Add(dutyInfo.CfcId);
|
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
|
||||||
else if (value == 2)
|
else if (value == 2)
|
||||||
Configuration.Duties.BlacklistedDutyCfcIds.Add(dutyInfo.CfcId);
|
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
|
||||||
|
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
@ -460,5 +492,6 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||||||
ushort SortKey,
|
ushort SortKey,
|
||||||
byte Index,
|
byte Index,
|
||||||
bool Enabled,
|
bool Enabled,
|
||||||
bool BossModEnabledByDefault);
|
bool BossModEnabledByDefault,
|
||||||
|
List<string> Notes);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user