add quest level filter to weapon list for mhgen
This commit is contained in:
@@ -174,6 +174,8 @@ def weapon_json(db, path):
|
||||
mkdirs_p(path)
|
||||
write_list_file(path, weapons)
|
||||
|
||||
item_stars = model.ItemStars(db)
|
||||
|
||||
all_data = []
|
||||
melodies = {}
|
||||
indexes = {}
|
||||
@@ -193,6 +195,12 @@ def weapon_json(db, path):
|
||||
]
|
||||
data["horn_melodies"] = melodies[w.horn_notes]
|
||||
|
||||
stars = item_stars.get_weapon_stars(w)
|
||||
data["village_stars"] = stars["Village"]
|
||||
data["guild_stars"] = stars["Guild"]
|
||||
data["permit_stars"] = stars["Permit"]
|
||||
data["arena_stars"] = stars["Arena"]
|
||||
|
||||
all_data.append(data)
|
||||
|
||||
with open(weapon_path, "w") as f:
|
||||
|
||||
@@ -1,99 +1,42 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
import _pathfix
|
||||
|
||||
from mhapi.db import MHDB, MHDBX
|
||||
from mhapi.model import get_costs
|
||||
|
||||
|
||||
def find_cost_level(db, c):
|
||||
monsters = { "HR": set(), "LR": set() }
|
||||
materials = { "HR": set(), "LR": set() }
|
||||
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
||||
for item in c["components"].keys():
|
||||
if item.startswith("HR ") or item.startswith("LR "):
|
||||
if not item.endswith(" Materials"):
|
||||
print "Error: bad item format '%s'" % item
|
||||
rank = item[:2]
|
||||
item = item[len("HR "):-len(" Materials")]
|
||||
monster = db.get_monster_by_name(item)
|
||||
if monster:
|
||||
monsters[rank].add(monster)
|
||||
#print "Monster", rank, monster.name, monster.id
|
||||
else:
|
||||
materials[rank].add(item)
|
||||
#print "Material", rank, item
|
||||
else:
|
||||
data = db.get_item_by_name(item)
|
||||
current_stars = find_item_level(db, data.id)
|
||||
# keep track of most 'expensive' item
|
||||
for k, v in current_stars.iteritems():
|
||||
if v is None:
|
||||
continue
|
||||
if stars[k] is None or v > stars[k]:
|
||||
stars[k] = v
|
||||
return stars
|
||||
|
||||
|
||||
def find_item_level(db, item_id):
|
||||
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
||||
|
||||
quests = db.get_item_quests(item_id)
|
||||
|
||||
gathering = db.get_item_gathering(item_id)
|
||||
gather_locations = set()
|
||||
for gather in gathering:
|
||||
gather_locations.add((gather["location_id"], gather["rank"]))
|
||||
for location_id, rank in list(gather_locations):
|
||||
gather_quests = db.get_location_quests(location_id, rank)
|
||||
quests.extend(gather_quests)
|
||||
|
||||
monsters = db.get_item_monsters(item_id)
|
||||
monster_ranks = set()
|
||||
for monster in monsters:
|
||||
monster_ranks.add((monster["monster_id"], monster["rank"]))
|
||||
for monster_id, rank in list(monster_ranks):
|
||||
monster_quests = db.get_monster_quests(monster_id, rank)
|
||||
quests.extend(monster_quests)
|
||||
|
||||
# find least expensive quest for getting the item
|
||||
for quest in quests:
|
||||
if quest.stars == 0:
|
||||
# ignore training quests
|
||||
if "Training" not in quest.name:
|
||||
print "Error: non training quest has 0 stars", \
|
||||
quest.id, quest.name
|
||||
continue
|
||||
if quest.hub in stars:
|
||||
current = stars[quest.hub]
|
||||
if current is None or quest.stars < current:
|
||||
stars[quest.hub] = quest.stars
|
||||
else:
|
||||
print "Error: unknown hub", quest.hub
|
||||
|
||||
return stars
|
||||
from mhapi.model import ItemStars
|
||||
|
||||
|
||||
def main():
|
||||
weapon_name = sys.argv[1]
|
||||
db = MHDB(game="gen", include_item_components=True)
|
||||
weapon = db.get_weapon_by_name(weapon_name)
|
||||
if weapon is None:
|
||||
print "Weapon '%s' not found" % weapon_name
|
||||
item_stars = ItemStars(db)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--item")
|
||||
parser.add_argument("-w", "--weapon")
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.item:
|
||||
item = db.get_item_by_name(args.item)
|
||||
if item is None:
|
||||
print "Item '%s' not found" % args.item
|
||||
sys.exit(1)
|
||||
if item.type == "Materials":
|
||||
stars = item_stars.get_material_stars(item.id)
|
||||
else:
|
||||
stars = item_stars.get_item_stars(item.id)
|
||||
elif args.weapon:
|
||||
weapon = db.get_weapon_by_name(args.weapon)
|
||||
if weapon is None:
|
||||
print "Weapon '%s' not found" % args.weapon
|
||||
sys.exit(1)
|
||||
stars = item_stars.get_weapon_stars(weapon)
|
||||
else:
|
||||
print "Specify -w or -i"
|
||||
sys.exit(1)
|
||||
|
||||
costs = get_costs(db, weapon)
|
||||
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
||||
# find least 'expensive' path
|
||||
for c in costs:
|
||||
current_stars = find_cost_level(db, c)
|
||||
for k, v in current_stars.iteritems():
|
||||
if v is None:
|
||||
continue
|
||||
if stars[k] is None or v < stars[k]:
|
||||
stars[k] = v
|
||||
for k, v in stars.iteritems():
|
||||
print k, v
|
||||
|
||||
|
||||
29
mhapi/db.py
29
mhapi/db.py
@@ -141,19 +141,26 @@ class MHDB(object):
|
||||
WHERE type IN (%s)
|
||||
""" % placeholders, tuple(args), model_cls=field_model("name"))
|
||||
|
||||
def get_items(self, item_types=None):
|
||||
def get_items(self, item_types=None, exclude_types=None):
|
||||
"""
|
||||
List of item objects.
|
||||
"""
|
||||
q = "SELECT * FROM items"
|
||||
args = []
|
||||
if item_types:
|
||||
item_types = sorted(item_types)
|
||||
placeholders = ", ".join(["?"] * len(item_types))
|
||||
q += "\nWHERE type IN (%s)" % placeholders
|
||||
item_types = tuple(item_types)
|
||||
args.extend(item_types)
|
||||
if exclude_types:
|
||||
exclude_types = sorted(exclude_types)
|
||||
placeholders = ", ".join(["?"] * len(exclude_types))
|
||||
q += "\nWHERE type NOT IN (%s)" % placeholders
|
||||
args.extend(exclude_types)
|
||||
else:
|
||||
item_types = ()
|
||||
return self._query_all("items", q, item_types, model_cls=model.Item)
|
||||
args = []
|
||||
args = tuple(args)
|
||||
return self._query_all("items", q, args, model_cls=model.Item)
|
||||
|
||||
def get_item(self, item_id):
|
||||
"""
|
||||
@@ -177,6 +184,8 @@ class MHDB(object):
|
||||
"""
|
||||
Single wyporium row or None.
|
||||
"""
|
||||
if self.game != "4u":
|
||||
return None
|
||||
return self._query_one("wyporium", """
|
||||
SELECT * FROM wyporium
|
||||
WHERE item_in_id=?
|
||||
@@ -514,6 +523,18 @@ class MHDB(object):
|
||||
WHERE notes=?
|
||||
""", (notes,), model_cls=model.HornMelody)
|
||||
|
||||
def get_material_items(self, material_item_id):
|
||||
"""
|
||||
Get dict rows of items that satisfy the given material, containing
|
||||
item_id and amount keys. MHGen only.
|
||||
"""
|
||||
assert self.game == "gen"
|
||||
return self._query_all("material_items", """
|
||||
SELECT item_id, amount FROM item_to_material
|
||||
WHERE item_to_material.material_item_id = ?
|
||||
ORDER BY amount ASC
|
||||
""", (material_item_id,))
|
||||
|
||||
def _add_components(self, key, item_results):
|
||||
"""
|
||||
Add component data to item results from _query_one or _query_all,
|
||||
|
||||
137
mhapi/model.py
137
mhapi/model.py
@@ -641,3 +641,140 @@ def get_costs(db, weapon):
|
||||
create_cost["components"][item.name] = item.quantity
|
||||
costs = [create_cost] + costs
|
||||
return costs
|
||||
|
||||
|
||||
class ItemStars(object):
|
||||
"""
|
||||
Get the game progress (in hub stars) required to make an item. Caches
|
||||
values.
|
||||
"""
|
||||
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self._item_stars = {} # item id -> stars dict
|
||||
self._weapon_stars = {} # weapon id -> stars dict
|
||||
|
||||
def get_weapon_stars(self, weapon):
|
||||
"""
|
||||
Get lowest star levels needed to make weapon, among the different
|
||||
paths available.
|
||||
"""
|
||||
stars = self._weapon_stars.get(weapon.id)
|
||||
if stars is not None:
|
||||
return stars
|
||||
|
||||
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
||||
costs = get_costs(self.db, weapon)
|
||||
# find least 'expensive' path
|
||||
for c in costs:
|
||||
current_stars = self._get_component_stars(c)
|
||||
for k, v in current_stars.iteritems():
|
||||
if v is None:
|
||||
continue
|
||||
if stars[k] is None or v < stars[k]:
|
||||
stars[k] = v
|
||||
self._weapon_stars[weapon.id] = stars
|
||||
return stars
|
||||
|
||||
def _get_component_stars(self, c):
|
||||
# need to track unititialized vs unavailable
|
||||
stars = dict(Village=0, Guild=0, Permit=0, Arena=0)
|
||||
for item_name in c["components"].keys():
|
||||
item = self.db.get_item_by_name(item_name)
|
||||
if item.type == "Materials":
|
||||
current_stars = self.get_material_stars(item.id)
|
||||
else:
|
||||
current_stars = self.get_item_stars(item.id)
|
||||
# keep track of most 'expensive' item
|
||||
for k, v in current_stars.items():
|
||||
if stars[k] is None:
|
||||
# another item was unavailable from the hub
|
||||
continue
|
||||
if v is None:
|
||||
if k == "Village" and current_stars["Guild"] is not None:
|
||||
# available from guild and not from village,
|
||||
# e.g. certain HR parts. Mark entire item as
|
||||
# unavailable from village, don't allow override.
|
||||
stars[k] = None
|
||||
continue
|
||||
if v > stars[k]:
|
||||
stars[k] = v
|
||||
|
||||
# check for hubs that had no candidate item, and null them out
|
||||
for k in list(stars.keys()):
|
||||
if stars[k] == 0:
|
||||
stars[k] = None
|
||||
return stars
|
||||
|
||||
def get_material_stars(self, material_item_id):
|
||||
"""
|
||||
Find the level of the cheapest item that satisfies the material
|
||||
that is not a scrap.
|
||||
"""
|
||||
stars = self._item_stars.get(material_item_id)
|
||||
if stars is not None:
|
||||
return stars
|
||||
|
||||
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
||||
rows = self.db.get_material_items(material_item_id)
|
||||
for row in rows:
|
||||
item = self.db.get_item(row["item_id"])
|
||||
if "Scrap" in item.name:
|
||||
continue
|
||||
stars = self.get_item_stars(item.id)
|
||||
break
|
||||
self._item_stars[material_item_id] = stars
|
||||
return stars
|
||||
|
||||
def get_item_stars(self, item_id):
|
||||
stars = self._item_stars.get(item_id)
|
||||
if stars is not None:
|
||||
return stars
|
||||
|
||||
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
||||
|
||||
quests = self.db.get_item_quests(item_id)
|
||||
|
||||
gathering = self.db.get_item_gathering(item_id)
|
||||
gather_locations = set()
|
||||
for gather in gathering:
|
||||
gather_locations.add((gather["location_id"], gather["rank"]))
|
||||
for location_id, rank in list(gather_locations):
|
||||
gather_quests = self.db.get_location_quests(location_id, rank)
|
||||
quests.extend(gather_quests)
|
||||
|
||||
monsters = self.db.get_item_monsters(item_id)
|
||||
monster_ranks = set()
|
||||
for monster in monsters:
|
||||
monster_ranks.add((monster["monster_id"], monster["rank"]))
|
||||
for monster_id, rank in list(monster_ranks):
|
||||
monster_quests = self.db.get_monster_quests(monster_id, rank)
|
||||
quests.extend(monster_quests)
|
||||
|
||||
# find least expensive quest for getting the item
|
||||
for quest in quests:
|
||||
if quest.stars == 0:
|
||||
# ignore training quests
|
||||
if "Training" not in quest.name:
|
||||
print "Error: non training quest has 0 stars", \
|
||||
quest.id, quest.name
|
||||
continue
|
||||
if quest.hub in stars:
|
||||
current = stars[quest.hub]
|
||||
if current is None or quest.stars < current:
|
||||
stars[quest.hub] = quest.stars
|
||||
else:
|
||||
print "Error: unknown hub", quest.hub
|
||||
|
||||
# if available guild or village, then null out permit/arena values,
|
||||
# because they are more useful for filtering if limited to items
|
||||
# exclusively available from permit or arena. Allows matching
|
||||
# on based on meeting specified critera for
|
||||
# (guild or village) and permit and arena.
|
||||
if stars["Village"] or stars["Guild"]:
|
||||
stars["Permit"] = None
|
||||
stars["Arena"] = None
|
||||
|
||||
self._item_stars[item_id] = stars
|
||||
return stars
|
||||
|
||||
|
||||
106
web/mhgen/recommends.html
Normal file
106
web/mhgen/recommends.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Poogie Recommends</title>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/themes/smoothness/jquery-ui.css" />
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script>
|
||||
|
||||
<script src="/js/common.js"></script>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
font-family: sans, sans-serif;
|
||||
}
|
||||
|
||||
#output { flex: 99 1 auto; }
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
var DATA_PATH = get_base_path() + "/rewards/";
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#search").click(update_search);
|
||||
$("#item").keypress(function(e) {
|
||||
if (e.which == 13) { update_search(); }
|
||||
});
|
||||
setup_item_autocomplete("#item");
|
||||
var item_name = $.QueryString["item"];
|
||||
if (item_name) {
|
||||
console.log("qs item: " + item_name);
|
||||
if (history.state && history.state["item_name"]) {
|
||||
item_name = history.state["item_name"];
|
||||
console.log("override qs with state item: " + item_name);
|
||||
}
|
||||
var normalized_name = normalize_name(item_name);
|
||||
var encoded_name = encodeURIComponent(normalized_name);
|
||||
display_item(normalized_name);
|
||||
console.log("replaceState: " + normalized_name);
|
||||
window.history.replaceState({ "item_name": normalized_name }, "",
|
||||
"/recommends.html?item="
|
||||
+ encoded_name );
|
||||
}
|
||||
$(window).on("popstate", function(e) {
|
||||
var oe = e.originalEvent;
|
||||
if (oe.state !== null) {
|
||||
console.log("popState:" + JSON.stringify(oe.state));
|
||||
display_item(oe.state["item_name"]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function update_search() {
|
||||
// update the item search based on the text field, and also push
|
||||
// the state to history
|
||||
var item_name = $.trim($("#item").val());
|
||||
var normalized_name = normalize_name(item_name);
|
||||
if (window.history.state
|
||||
&& window.history.state["item_name"] == normalized_name) {
|
||||
console.log("item not changed, skipping update");
|
||||
return;
|
||||
}
|
||||
var encoded_name = encodeURIComponent(normalized_name);
|
||||
display_item(normalized_name);
|
||||
console.log("pushState: " + normalized_name);
|
||||
window.history.pushState({ "item_name": normalized_name }, "",
|
||||
"/recommends.html?item=" + encoded_name );
|
||||
}
|
||||
|
||||
function display_item(normalized_name) {
|
||||
// display the exact item name if available; does not push state
|
||||
$("#item").val(normalized_name);
|
||||
var encoded_name = encodeURIComponent(normalized_name);
|
||||
|
||||
$.get(DATA_PATH + encoded_name + ".txt",
|
||||
function(data) {
|
||||
$("#output").text(data);
|
||||
}).fail(
|
||||
function() {
|
||||
$("#output").text("Error: item '"
|
||||
+ normalized_name + "' not found");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex">
|
||||
<div>
|
||||
<label for="item">Item:</label>
|
||||
<input id="item" type="text" size="20" />
|
||||
<button id="search">Ask Poogie</button>
|
||||
<a href="https://github.com/bd4/monster-hunter-scripts/blob/master/RECOMMENDATIONS.adoc">Understanding Results</a>
|
||||
(<a href="https://github.com/bd4/monster-hunter-scripts">source</a>)
|
||||
</div>
|
||||
<br />
|
||||
<textarea readonly="true" rows="10" cols="80" id="output"></textarea>
|
||||
</div>
|
||||
</body>
|
||||
@@ -170,14 +170,27 @@
|
||||
return { "weapon_type": $("#weapon_type").val(),
|
||||
"weapon_element": $("#weapon_element").val(),
|
||||
"weapon_final": $("#weapon_final").is(":checked"),
|
||||
"weapon_name_text": $("#weapon_name_text").val() };
|
||||
"weapon_name_text": $("#weapon_name_text").val(),
|
||||
"village_stars": $("#village_stars").val(),
|
||||
"guild_stars": $("#guild_stars").val(),
|
||||
"permit_stars": $("#permit_stars").val(),
|
||||
"arena_stars": $("#arena_stars").val() };
|
||||
}
|
||||
|
||||
function load_state(state) {
|
||||
$("#weapon_type").val(state["weapon_type"]);
|
||||
$("#weapon_element").val(state["weapon_element"]);
|
||||
final = state["weapon_final"];
|
||||
if (typeof final == "string") {
|
||||
final = final.toLowerCase();
|
||||
state["weapon_final"] = (final == "true" || final == "1");
|
||||
}
|
||||
$("#weapon_final").prop("checked", state["weapon_final"]);
|
||||
$("#weapon_name_text").val(state["weapon_name_text"]);
|
||||
$("#village_stars").val(state["village_stars"]);
|
||||
$("#guild_stars").val(state["guild_stars"]);
|
||||
$("#permit_stars").val(state["permit_stars"]);
|
||||
$("#arena_stars").val(state["arena_stars"]);
|
||||
}
|
||||
|
||||
function save_state(state, replace) {
|
||||
@@ -189,11 +202,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
function match_stars(match_value, weapon_value) {
|
||||
// NOTE: a null weapon_value can be not available, or no data
|
||||
// available (should probably fix this)
|
||||
if (match_value == "Any") {
|
||||
return true;
|
||||
}
|
||||
if (match_value == "None") {
|
||||
// for None, allow null values, because null can be no requirements
|
||||
// or no data available
|
||||
if (weapon_value != null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// if matching a specific value, require a non-null weapon value
|
||||
if (weapon_value == null) {
|
||||
return false;
|
||||
}
|
||||
match_value = parseInt(match_value);
|
||||
if (weapon_value > match_value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function weapon_predicate(state, weapon_data) {
|
||||
var weapon_type = state["weapon_type"];
|
||||
var weapon_element = state["weapon_element"];
|
||||
var final_only = state["weapon_final"];
|
||||
var weapon_names = state["weapon_name_text"].split("|");
|
||||
var village_stars = state["village_stars"];
|
||||
var guild_stars = state["guild_stars"];
|
||||
var permit_stars = state["permit_stars"];
|
||||
var arena_stars = state["arena_stars"];
|
||||
|
||||
// allow satisfying quest filter with village or guild, since they
|
||||
// involve essentially the same quests, rewards, and mosnters,
|
||||
// but if permit or arena filters are set, they must be satisfied
|
||||
// independently
|
||||
if (! match_stars(village_stars, weapon_data["village_stars"])
|
||||
&& ! match_stars(guild_stars, weapon_data["guild_stars"])) {
|
||||
return false;
|
||||
}
|
||||
if (! match_stars(permit_stars, weapon_data["permit_stars"])) {
|
||||
return false;
|
||||
}
|
||||
if (! match_stars(village_stars, weapon_data["village_stars"])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (final_only && weapon_data["final"] != 1) {
|
||||
return false;
|
||||
@@ -321,6 +378,55 @@
|
||||
<td><input id="weapon_final" type="checkbox" /></td>
|
||||
<td><button id="search">Search</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<label>Quest Level:</label>
|
||||
<select id="village_stars">
|
||||
<option value="Any">Village Any</option>
|
||||
<option value="1">Village 1*</option>
|
||||
<option value="2">Village 2*</option>
|
||||
<option value="3">Village 3*</option>
|
||||
<option value="4">Village 4*</option>
|
||||
<option value="5">Village 5*</option>
|
||||
<option value="6">Village 6*</option>
|
||||
</select>
|
||||
<select id="guild_stars">
|
||||
<option value="Any">Guild Any</option>
|
||||
<option value="1">Guild 1*</option>
|
||||
<option value="2">Guild 2*</option>
|
||||
<option value="3">Guild 3*</option>
|
||||
<option value="4">Guild 4*</option>
|
||||
<option value="5">Guild 5*</option>
|
||||
<option value="6">Guild 6*</option>
|
||||
<option value="7">Guild 7*</option>
|
||||
</select>
|
||||
<select id="permit_stars">
|
||||
<option value="Any">Permit Any</option>
|
||||
<option value="None">Permit None</option>
|
||||
<option value="1">Permit 1*</option>
|
||||
<option value="2">Permit 2*</option>
|
||||
<option value="3">Permit 3*</option>
|
||||
<option value="4">Permit 4*</option>
|
||||
<option value="5">Permit 5*</option>
|
||||
<option value="6">Permit 6*</option>
|
||||
<option value="7">Permit 7*</option>
|
||||
<option value="8">Permit 8*</option>
|
||||
<option value="9">Permit 9*</option>
|
||||
<option value="10">Permit 10*</option>
|
||||
</select>
|
||||
<select id="arena_stars">
|
||||
<option value="Any">Arena Any</option>
|
||||
<option value="None">Arena None</option>
|
||||
<option value="1">Arena 1*</option>
|
||||
<option value="2">Arena 2*</option>
|
||||
<option value="3">Arena 3*</option>
|
||||
<option value="4">Arena 4*</option>
|
||||
<option value="5">Arena 5*</option>
|
||||
<option value="6">Arena 6*</option>
|
||||
<option value="7">Arena 7*</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<label for="weapon_name_text"
|
||||
|
||||
Reference in New Issue
Block a user