Erstellen der mmr_rules Matrix. Berechnung der Matches, erster Teil. Aufsetzten der MMR Berechnung.
This commit is contained in:
parent
cd2050e6fe
commit
0f914f3117
|
|
@ -1 +1 @@
|
|||
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel Nagel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}
|
||||
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Regelvergesser","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}
|
||||
|
|
@ -99,9 +99,49 @@ def get_all_players_from_system(system_name):
|
|||
|
||||
return result
|
||||
|
||||
def get_gamesystem_id_by_name(system_name):
|
||||
"""Holt die interne ID eines Spielsystems anhand seines Namens (z.B. 'Warhammer 40k' -> 1)."""
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
cursor = connection.cursor()
|
||||
|
||||
import sqlite3
|
||||
from data.setup_database import DB_PATH
|
||||
# Wir suchen exakt nach dem Namen
|
||||
cursor.execute("SELECT id FROM gamesystems WHERE name = ?", (system_name,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
connection.close()
|
||||
|
||||
# Wenn wir einen Treffer haben, geben wir die erste Spalte (die ID) zurück
|
||||
if row:
|
||||
return row[0]
|
||||
|
||||
# Wenn das System nicht existiert
|
||||
return None
|
||||
|
||||
|
||||
def get_player_rank(player_id, gamesystem_id):
|
||||
"""Sortiert die Liga nach MMR und gibt den aktuellen Platz (Rang) des Spielers zurück."""
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
connection.row_factory = sqlite3.Row
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Alle Spieler dieser Liga, sortiert nach MMR (höchstes zuerst)
|
||||
query = """
|
||||
SELECT player_id
|
||||
FROM player_game_statistic
|
||||
WHERE gamesystem_id = ?
|
||||
ORDER BY mmr DESC
|
||||
"""
|
||||
cursor.execute(query, (gamesystem_id,))
|
||||
rows = cursor.fetchall()
|
||||
connection.close()
|
||||
|
||||
# Wir zählen die Liste durch (enumerate fängt bei 0 an, also machen wir +1 für den Rang)
|
||||
for index, row in enumerate(rows):
|
||||
if row['player_id'] == player_id:
|
||||
return index + 1
|
||||
|
||||
# Falls der Spieler nicht in der Liga ist (sollte nicht passieren)
|
||||
return 999
|
||||
|
||||
|
||||
|
||||
|
|
@ -224,7 +264,7 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
|
|||
# Sicherheitscheck (sollte eigentlich nie passieren)
|
||||
if not sys_row:
|
||||
connection.close()
|
||||
return False
|
||||
return None
|
||||
|
||||
sys_id = sys_row[0]
|
||||
|
||||
|
|
@ -235,11 +275,12 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
|
|||
"""
|
||||
|
||||
cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2))
|
||||
new_match_id = cursor.lastrowid
|
||||
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
return True
|
||||
return new_match_id
|
||||
|
||||
|
||||
|
||||
|
|
@ -306,3 +347,32 @@ def get_last_20_match_scores(player_id, system_name):
|
|||
return {"points": points_list, "labels": labels_list}
|
||||
|
||||
|
||||
|
||||
def get_match_by_id(match_id):
|
||||
"""Holt alle Daten eines spezifischen Matches anhand der Match-ID."""
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
connection.row_factory = sqlite3.Row # Damit wir wieder ein schönes Dictionary bekommen
|
||||
cursor = connection.cursor()
|
||||
|
||||
# m.* holt alle Spalten aus dem Match (Punkte, IDs, Datum)
|
||||
# sys.name AS gamesystem_name holt uns direkt den passenden Text für deine JSON-Ladefunktion
|
||||
query = """
|
||||
SELECT m.*, sys.name AS gamesystem_name
|
||||
FROM matches m
|
||||
JOIN gamesystems sys ON m.gamesystem_id = sys.id
|
||||
WHERE m.id = ?
|
||||
"""
|
||||
|
||||
cursor.execute(query, (match_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
connection.close()
|
||||
|
||||
# Wenn ein Match gefunden wurde, geben wir es als Dictionary zurück
|
||||
if row:
|
||||
return dict(row)
|
||||
|
||||
return None # Falls die ID nicht existiert
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
DB_PATH = "data/warhammer_league.db"
|
||||
|
||||
|
|
@ -117,6 +119,7 @@ def seed_gamesystems():
|
|||
print("Spielsysteme angelegt!")
|
||||
|
||||
#Nächster Schritt: Standard Achievments eintragen.
|
||||
generate_default_mmr_rules()
|
||||
seed_achievements()
|
||||
|
||||
def seed_achievements():
|
||||
|
|
@ -179,3 +182,75 @@ def seed_dummy_player():
|
|||
connection.close()
|
||||
print("Test-Gegner 'Dummy Mc DummDumm' ist bereit und in allen Ligen angemeldet!")
|
||||
|
||||
|
||||
|
||||
def generate_default_mmr_rules():
|
||||
"""Erstellt für jedes Spielsystem eine Standard-JSON-Datei, falls sie noch nicht existiert."""
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.execute("SELECT name FROM gamesystems")
|
||||
systems = cursor.fetchall()
|
||||
connection.close()
|
||||
|
||||
# 1. Sicherstellen, dass der Ordner existiert (Best Practice!)
|
||||
folder_path = "mmr_calculations"
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
|
||||
# 2. Wir gehen jedes gefundene Spielsystem durch
|
||||
for sys in systems:
|
||||
sys_name = sys[0]
|
||||
|
||||
# Wir machen den Namen "dateisicher" (z.B. "Warhammer 40k" -> "warhammer_40k")
|
||||
safe_name = sys_name.replace(" ", "_").lower()
|
||||
file_path = os.path.join(folder_path, f"mmr_rules_{safe_name}.json")
|
||||
|
||||
# 3. SICHERHEITS-CHECK: Existiert die Datei schon?
|
||||
# Wenn ja, ignorieren wir sie, damit wir dein händisches Balancing nicht überschreiben!
|
||||
if not os.path.exists(file_path):
|
||||
|
||||
# Das ist unsere Standard-Vorlage (Faktor 10)
|
||||
default_rules = {
|
||||
"system_info": f"Balancing für {sys_name}",
|
||||
"draw_point_difference": 5,
|
||||
"rank_matrix": {
|
||||
"10": {"win": 10, "draw": 30},
|
||||
"9": {"win": 10, "draw": 30},
|
||||
"8": {"win": 10, "draw": 20},
|
||||
"7": {"win": 10, "draw": 20},
|
||||
"6": {"win": 20, "draw": 10},
|
||||
"5": {"win": 20, "draw": 10},
|
||||
"4": {"win": 20, "draw": 0},
|
||||
"3": {"win": 20, "draw": 0},
|
||||
"2": {"win": 30, "draw": 0},
|
||||
"1": {"win": 30, "draw": 0},
|
||||
"0": {"win": 30, "draw": 0},
|
||||
"-1": {"win": 30, "draw": 0},
|
||||
"-2": {"win": 40, "draw": 0},
|
||||
"-3": {"win": 40, "draw": -10},
|
||||
"-4": {"win": 50, "draw": -20},
|
||||
"-5": {"win": 60, "draw": -20},
|
||||
"-6": {"win": 70, "draw": -20},
|
||||
"-7": {"win": 80, "draw": -50},
|
||||
"-8": {"win": 100, "draw": -50},
|
||||
"-9": {"win": 120, "draw": -60},
|
||||
"-10": {"win": 150, "draw": -60}
|
||||
},
|
||||
"score_bonus": [
|
||||
{"min_diff": 95, "bonus": 40},
|
||||
{"min_diff": 85, "bonus": 30},
|
||||
{"min_diff": 65, "bonus": 20},
|
||||
{"min_diff": 35, "bonus": 10},
|
||||
{"min_diff": 0, "bonus": 0}
|
||||
]
|
||||
}
|
||||
|
||||
# 4. JSON-Datei schreiben
|
||||
# 'w' steht für write (schreiben). indent=4 macht es für Menschen schön lesbar formatiert.
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_rules, f, indent=4, ensure_ascii=False)
|
||||
|
||||
print(f"Neu: Balancing-Datei für '{sys_name}' wurde erstellt -> {file_path}")
|
||||
else:
|
||||
print(f"OK: Balancing-Datei für '{sys_name}' existiert bereits.")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from nicegui import ui, app
|
||||
from gui import gui_style
|
||||
from data import data_api
|
||||
from mmr_calculations import calc_match
|
||||
|
||||
def setup_routes():
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ def setup_routes():
|
|||
|
||||
ui.space()
|
||||
|
||||
# Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern.
|
||||
def input_match_to_database():
|
||||
# 1. Prüfen, ob ein Gegner ausgewählt wurde
|
||||
p2_id = opponent_select.value
|
||||
|
|
@ -66,10 +68,11 @@ def setup_routes():
|
|||
score_p2 = p2_points.value
|
||||
|
||||
# 3. Daten an die API schicken (my_id haben wir oben schon von app.storage.user geholt)
|
||||
data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2)
|
||||
match_id = data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2)
|
||||
|
||||
# 4. Erfolgsmeldung und zurück zur Übersicht
|
||||
ui.notify("Match erfolgreich eingetragen!", color="green")
|
||||
calc_match.calculate_inserted_match(systemname, match_id)
|
||||
ui.navigate.to('/')
|
||||
|
||||
with ui.row().classes("w-full items-center justify-between"):
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
from data import database
|
||||
62
mmr_calculations/calc_match.py
Normal file
62
mmr_calculations/calc_match.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from data import data_api
|
||||
from mmr_calculations import *
|
||||
|
||||
# Mach die DB abfrage für die Relevanten Daten. Von hier aus werden die "Aufgaben" und Daten dann an die kleineren Berechnungs Funktionen verteilt.
|
||||
def calculate_inserted_match (gamesystem, match_id):
|
||||
match_data = data_api.get_match_by_id(match_id)
|
||||
if not match_data:
|
||||
print("Fehler: Match nicht gefunden!")
|
||||
return
|
||||
# Laden und umsetzen der Match Daten
|
||||
p1_id = match_data['player1_id']
|
||||
p2_id = match_data['player2_id']
|
||||
p1_score = match_data['score_player1']
|
||||
p2_score = match_data['score_player2']
|
||||
sys_name = match_data['gamesystem_name']
|
||||
sys_id = match_data['gamesystem_id']
|
||||
|
||||
rules = load_mmr_rule_matrix(systemname)
|
||||
draw_diff = rules["draw_point_difference"]
|
||||
|
||||
calculated = False
|
||||
winner_id = None
|
||||
looser_id = None
|
||||
match_is_draw = False
|
||||
winner_score = 0
|
||||
looser_score = 0
|
||||
|
||||
# Abgrenzen ob das Match schon berechnet wurde. Weil ein Draw kan 4 Punkte unterschied haben
|
||||
# 43-41 ist ein Draw aber rein Mathematisch würde es auch ein anderes if triggern
|
||||
while not calculated:
|
||||
# Match is a Draw
|
||||
if -draw_diff <= (p1_score-p2_score) <= draw_diff:
|
||||
match_is_draw = True
|
||||
winner_id = p1_id
|
||||
looser_id = p2_id
|
||||
winner_score = p1_score
|
||||
looser_score = p2_score
|
||||
|
||||
calculated = True
|
||||
break
|
||||
|
||||
# p1 ist der Sieger.
|
||||
if score_p1 > score_p2:
|
||||
winner_id = p1_id
|
||||
looser_id = p2_id
|
||||
winner_score = p1_score
|
||||
looser_score = p2_score
|
||||
|
||||
calculated = True
|
||||
break
|
||||
|
||||
# p2 ist der Sieger.
|
||||
if score_p1 < score_p2:
|
||||
winner_id = p2_id
|
||||
looser_id = p1_id
|
||||
winner_score = p2_score
|
||||
looser_score = p1_score
|
||||
|
||||
calculated = True
|
||||
break
|
||||
|
||||
calc_mmr_change.calc_mmr_change(sys_name, winner_id, looser_id, winner_score, looser_score, match_is_draw)
|
||||
29
mmr_calculations/calc_mmr_change.py
Normal file
29
mmr_calculations/calc_mmr_change.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from data import data_api
|
||||
import json
|
||||
import os
|
||||
|
||||
def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw):
|
||||
gamesystem_id = data_api.get_gamesystem_id_by_name(systemname)
|
||||
winner_rank = data_api.get_player_rank(winner_id,gamesystem_id)
|
||||
looser_rank = data_api.get_player_rank(looser_id, gamesystem_id)
|
||||
|
||||
rules = load_mmr_rule_matrix(systemname)
|
||||
|
||||
if match_is_draw:
|
||||
mmr_change = rules["rank_matrix"][str(winner_rank-looser_rank)]["draw"]
|
||||
|
||||
else:
|
||||
mmr_change = rules["rank_matrix"][str(winner_rank-looser_rank)]["win"]
|
||||
|
||||
|
||||
|
||||
|
||||
def load_mmr_rule_matrix(systemname):
|
||||
safe_name = systemname.replace(" ", "_").lower()
|
||||
|
||||
file_path = f"mmr_calculations/mmr_rules_{safe_name}.json"
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
rules = json.load(file)
|
||||
|
||||
return rules
|
||||
|
||||
112
mmr_calculations/mmr_rules_spearhead.json
Normal file
112
mmr_calculations/mmr_rules_spearhead.json
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"system_info": "Balancing für Spearhead",
|
||||
"draw_point_difference": 5,
|
||||
"rank_matrix": {
|
||||
"10": {
|
||||
"win": 10,
|
||||
"draw": 30
|
||||
},
|
||||
"9": {
|
||||
"win": 10,
|
||||
"draw": 30
|
||||
},
|
||||
"8": {
|
||||
"win": 10,
|
||||
"draw": 20
|
||||
},
|
||||
"7": {
|
||||
"win": 10,
|
||||
"draw": 20
|
||||
},
|
||||
"6": {
|
||||
"win": 20,
|
||||
"draw": 10
|
||||
},
|
||||
"5": {
|
||||
"win": 20,
|
||||
"draw": 10
|
||||
},
|
||||
"4": {
|
||||
"win": 20,
|
||||
"draw": 0
|
||||
},
|
||||
"3": {
|
||||
"win": 20,
|
||||
"draw": 0
|
||||
},
|
||||
"2": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"1": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"0": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"-1": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"-2": {
|
||||
"win": 40,
|
||||
"draw": 0
|
||||
},
|
||||
"-3": {
|
||||
"win": 40,
|
||||
"draw": -10
|
||||
},
|
||||
"-4": {
|
||||
"win": 50,
|
||||
"draw": -20
|
||||
},
|
||||
"-5": {
|
||||
"win": 60,
|
||||
"draw": -20
|
||||
},
|
||||
"-6": {
|
||||
"win": 70,
|
||||
"draw": -20
|
||||
},
|
||||
"-7": {
|
||||
"win": 80,
|
||||
"draw": -50
|
||||
},
|
||||
"-8": {
|
||||
"win": 100,
|
||||
"draw": -50
|
||||
},
|
||||
"-9": {
|
||||
"win": 120,
|
||||
"draw": -60
|
||||
},
|
||||
"-10": {
|
||||
"win": 150,
|
||||
"draw": -60
|
||||
}
|
||||
},
|
||||
"score_bonus": [
|
||||
{
|
||||
"min_diff": 95,
|
||||
"bonus": 40
|
||||
},
|
||||
{
|
||||
"min_diff": 85,
|
||||
"bonus": 30
|
||||
},
|
||||
{
|
||||
"min_diff": 65,
|
||||
"bonus": 20
|
||||
},
|
||||
{
|
||||
"min_diff": 35,
|
||||
"bonus": 10
|
||||
},
|
||||
{
|
||||
"min_diff": 0,
|
||||
"bonus": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
112
mmr_calculations/mmr_rules_warhammer_40k.json
Normal file
112
mmr_calculations/mmr_rules_warhammer_40k.json
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"system_info": "Balancing für Warhammer 40k",
|
||||
"draw_point_difference": 5,
|
||||
"rank_matrix": {
|
||||
"10": {
|
||||
"win": 10,
|
||||
"draw": 30
|
||||
},
|
||||
"9": {
|
||||
"win": 10,
|
||||
"draw": 30
|
||||
},
|
||||
"8": {
|
||||
"win": 10,
|
||||
"draw": 20
|
||||
},
|
||||
"7": {
|
||||
"win": 10,
|
||||
"draw": 20
|
||||
},
|
||||
"6": {
|
||||
"win": 20,
|
||||
"draw": 10
|
||||
},
|
||||
"5": {
|
||||
"win": 20,
|
||||
"draw": 10
|
||||
},
|
||||
"4": {
|
||||
"win": 20,
|
||||
"draw": 0
|
||||
},
|
||||
"3": {
|
||||
"win": 20,
|
||||
"draw": 0
|
||||
},
|
||||
"2": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"1": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"0": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"-1": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"-2": {
|
||||
"win": 40,
|
||||
"draw": 0
|
||||
},
|
||||
"-3": {
|
||||
"win": 40,
|
||||
"draw": -10
|
||||
},
|
||||
"-4": {
|
||||
"win": 50,
|
||||
"draw": -20
|
||||
},
|
||||
"-5": {
|
||||
"win": 60,
|
||||
"draw": -20
|
||||
},
|
||||
"-6": {
|
||||
"win": 70,
|
||||
"draw": -20
|
||||
},
|
||||
"-7": {
|
||||
"win": 80,
|
||||
"draw": -50
|
||||
},
|
||||
"-8": {
|
||||
"win": 100,
|
||||
"draw": -50
|
||||
},
|
||||
"-9": {
|
||||
"win": 120,
|
||||
"draw": -60
|
||||
},
|
||||
"-10": {
|
||||
"win": 150,
|
||||
"draw": -60
|
||||
}
|
||||
},
|
||||
"score_bonus": [
|
||||
{
|
||||
"min_diff": 95,
|
||||
"bonus": 40
|
||||
},
|
||||
{
|
||||
"min_diff": 85,
|
||||
"bonus": 30
|
||||
},
|
||||
{
|
||||
"min_diff": 65,
|
||||
"bonus": 20
|
||||
},
|
||||
{
|
||||
"min_diff": 35,
|
||||
"bonus": 10
|
||||
},
|
||||
{
|
||||
"min_diff": 0,
|
||||
"bonus": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
112
mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json
Normal file
112
mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"system_info": "Balancing für Warhammer Age of Sigmar",
|
||||
"draw_point_difference": 5,
|
||||
"rank_matrix": {
|
||||
"10": {
|
||||
"win": 10,
|
||||
"draw": 30
|
||||
},
|
||||
"9": {
|
||||
"win": 10,
|
||||
"draw": 30
|
||||
},
|
||||
"8": {
|
||||
"win": 10,
|
||||
"draw": 20
|
||||
},
|
||||
"7": {
|
||||
"win": 10,
|
||||
"draw": 20
|
||||
},
|
||||
"6": {
|
||||
"win": 20,
|
||||
"draw": 10
|
||||
},
|
||||
"5": {
|
||||
"win": 20,
|
||||
"draw": 10
|
||||
},
|
||||
"4": {
|
||||
"win": 20,
|
||||
"draw": 0
|
||||
},
|
||||
"3": {
|
||||
"win": 20,
|
||||
"draw": 0
|
||||
},
|
||||
"2": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"1": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"0": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"-1": {
|
||||
"win": 30,
|
||||
"draw": 0
|
||||
},
|
||||
"-2": {
|
||||
"win": 40,
|
||||
"draw": 0
|
||||
},
|
||||
"-3": {
|
||||
"win": 40,
|
||||
"draw": -10
|
||||
},
|
||||
"-4": {
|
||||
"win": 50,
|
||||
"draw": -20
|
||||
},
|
||||
"-5": {
|
||||
"win": 60,
|
||||
"draw": -20
|
||||
},
|
||||
"-6": {
|
||||
"win": 70,
|
||||
"draw": -20
|
||||
},
|
||||
"-7": {
|
||||
"win": 80,
|
||||
"draw": -50
|
||||
},
|
||||
"-8": {
|
||||
"win": 100,
|
||||
"draw": -50
|
||||
},
|
||||
"-9": {
|
||||
"win": 120,
|
||||
"draw": -60
|
||||
},
|
||||
"-10": {
|
||||
"win": 150,
|
||||
"draw": -60
|
||||
}
|
||||
},
|
||||
"score_bonus": [
|
||||
{
|
||||
"min_diff": 95,
|
||||
"bonus": 40
|
||||
},
|
||||
{
|
||||
"min_diff": 85,
|
||||
"bonus": 30
|
||||
},
|
||||
{
|
||||
"min_diff": 65,
|
||||
"bonus": 20
|
||||
},
|
||||
{
|
||||
"min_diff": 35,
|
||||
"bonus": 10
|
||||
},
|
||||
{
|
||||
"min_diff": 0,
|
||||
"bonus": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user