generating main roads in cities
This commit is contained in:
parent
62d81ca2ca
commit
56690bbb75
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Assets.Common
|
namespace Assets.Common
|
||||||
{
|
{
|
||||||
@ -17,5 +18,13 @@ namespace Assets.Common
|
|||||||
{
|
{
|
||||||
return Math.Sqrt(Math.Pow(a.x - b.x, 2) + Math.Pow(a.y - b.y, 2));
|
return Math.Sqrt(Math.Pow(a.x - b.x, 2) + Math.Pow(a.y - b.y, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
var hashCode = 1502939027;
|
||||||
|
hashCode = hashCode * -1521134295 + x.GetHashCode();
|
||||||
|
hashCode = hashCode * -1521134295 + y.GetHashCode();
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,6 +34,7 @@ namespace Assets.Editor
|
|||||||
generator.NewCity();
|
generator.NewCity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using Assets.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -8,7 +9,7 @@ namespace Assets.Map
|
|||||||
public class Graph<T>
|
public class Graph<T>
|
||||||
{
|
{
|
||||||
public List<T> Vertices { get; internal set; } = new List<T>();
|
public List<T> Vertices { get; internal set; } = new List<T>();
|
||||||
public Dictionary<int, List<int>> _edges = new Dictionary<int, List<int>>();
|
private Dictionary<int, List<int>> _edges = new Dictionary<int, List<int>>();
|
||||||
|
|
||||||
public IEnumerable<(int, int)> Edges
|
public IEnumerable<(int, int)> Edges
|
||||||
{
|
{
|
||||||
@ -38,11 +39,69 @@ namespace Assets.Map
|
|||||||
_edges[b].Add(a);
|
_edges[b].Add(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Graph<U> Morph<U>(Func<T, U> morph)
|
public void DeleteEdge(int a, int b)
|
||||||
|
{
|
||||||
|
if (!_edges.ContainsKey(a))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_edges.ContainsKey(b))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_edges[a].Remove(b);
|
||||||
|
_edges[b].Remove(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasCycle(Graph<T> graph)
|
||||||
|
{
|
||||||
|
int Find(int[] parent, int i)
|
||||||
|
{
|
||||||
|
if (parent[i] == -1)
|
||||||
|
return i;
|
||||||
|
|
||||||
|
return Find(parent, parent[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Union(int[] parent, int x, int y)
|
||||||
|
{
|
||||||
|
int xset = Find(parent, x);
|
||||||
|
int yset = Find(parent, y);
|
||||||
|
parent[xset] = yset;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] trueParent = new int[graph.Vertices.Count()];
|
||||||
|
|
||||||
|
for (int i = 0; i < graph.Vertices.Count(); ++i)
|
||||||
|
trueParent[i] = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < graph.Edges.Count(); ++i)
|
||||||
|
{
|
||||||
|
int x = Find(trueParent, graph.Edges.ElementAt(i).Item1);
|
||||||
|
int y = Find(trueParent, graph.Edges.ElementAt(i).Item2);
|
||||||
|
|
||||||
|
if (x == y)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Union(trueParent, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Graph<U> Morph<U>(Func<T, U> vertices)
|
||||||
{
|
{
|
||||||
var result = new Graph<U>() { Edges = Edges };
|
var result = new Graph<U>() { Edges = Edges };
|
||||||
|
|
||||||
result.Vertices.AddRange(Vertices.Select(morph));
|
result.Vertices.AddRange(Vertices.Select(vertices));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Graph<U, E> Morph<U, E>(Func<T, U> vertices, Func<(int, int), E> edges)
|
||||||
|
{
|
||||||
|
var result = new Graph<U, E>() { Edges = Edges };
|
||||||
|
|
||||||
|
result._edgeData = Edges.ToDictionary(k => k, v => edges(v));
|
||||||
|
result.Vertices.AddRange(Vertices.Select(vertices));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -54,4 +113,43 @@ namespace Assets.Map
|
|||||||
return _edges[vertex];
|
return _edges[vertex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Graph<T, E> : Graph<T>
|
||||||
|
{
|
||||||
|
internal Dictionary<(int, int), E> _edgeData = new Dictionary<(int, int), E>();
|
||||||
|
|
||||||
|
public void AddEdge(int a, int b, E data)
|
||||||
|
{
|
||||||
|
AddEdge(a, b);
|
||||||
|
SetEdgeData((a, b), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetEdgeData((int, int) edge, E data)
|
||||||
|
{
|
||||||
|
var (a, b) = edge;
|
||||||
|
|
||||||
|
if (a > b) (a, b) = (b, a);
|
||||||
|
|
||||||
|
_edgeData[(a, b)] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public E GetEdgeData((int, int) edge)
|
||||||
|
{
|
||||||
|
var (a, b) = edge;
|
||||||
|
|
||||||
|
if (a > b) (a, b) = (b, a);
|
||||||
|
|
||||||
|
return _edgeData[(a, b)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Graph<U, E2> Morph<U, E2>(Func<T, U> vertices, Func<E, (int, int), E2> edges)
|
||||||
|
{
|
||||||
|
var result = new Graph<U, E2>() { Edges = Edges };
|
||||||
|
|
||||||
|
result._edgeData = _edgeData.ToDictionary(e => e.Key, e => edges(e.Value, e.Key));
|
||||||
|
result.Vertices.AddRange(Vertices.Select(vertices));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -10,12 +10,33 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace Assets.Scripts
|
namespace Assets.Scripts
|
||||||
{
|
{
|
||||||
|
public class PointProximityComparer : IEqualityComparer<Point>
|
||||||
|
{
|
||||||
|
double _threshold;
|
||||||
|
|
||||||
|
public PointProximityComparer(double threshold = 0.2)
|
||||||
|
{
|
||||||
|
_threshold = threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(Point x, Point y)
|
||||||
|
{
|
||||||
|
return Point.Dist(x, y) < _threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(Point obj)
|
||||||
|
{
|
||||||
|
return obj.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class City
|
public class City
|
||||||
{
|
{
|
||||||
public Graph<Voronoi.Site> startGraph { get; set; }
|
public Graph<Voronoi.Site> startGraph { get; set; }
|
||||||
public List<Voronoi.Site> sitesList { get; set; }
|
public List<Voronoi.Site> sitesList { get; set; }
|
||||||
public Graph<Point> roads { get; set; } = new Graph<Point>();
|
public Graph<Point, double> roads { get; set; } = new Graph<Point, double>();
|
||||||
public List<(int, int)> boundariesEdges { get; set; }
|
public List<(int, int)> boundariesEdges { get; set; }
|
||||||
|
public Graph<Point> mainRoad { get; set; } = new Graph<Point>();
|
||||||
|
|
||||||
public City(Graph<Site> startGraph)
|
public City(Graph<Site> startGraph)
|
||||||
{
|
{
|
||||||
@ -38,19 +59,20 @@ namespace Assets.Scripts
|
|||||||
private void FixBoundaryEdges(Site site)
|
private void FixBoundaryEdges(Site site)
|
||||||
{
|
{
|
||||||
var a = boundariesEdges;
|
var a = boundariesEdges;
|
||||||
var b = site.Edges.Select(x => x.Item1 < x.Item2 ? x : (x.Item2, x.Item1));
|
var b = site.Edges.Select(x => x.Item1 < x.Item2 ? x : (x.Item2, x.Item1));
|
||||||
|
|
||||||
boundariesEdges = a.Union(b).Except(a.Intersect(b)).ToList();
|
boundariesEdges = a.Union(b).Except(a.Intersect(b)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateRoadGraph(IList<Point> points)
|
public void CreateRoadGraph(IList<Point> points)
|
||||||
{
|
{
|
||||||
roads = new Graph<Point>
|
VoronoiGenerator generator = new VoronoiGenerator(sitesList.SelectMany(site => site.Vertices.Select(i => points[i]).Append(site.Point)).Distinct(new PointProximityComparer(4)).ToList());
|
||||||
{
|
generator.Generate();
|
||||||
Vertices = sitesList.SelectMany(site => site.Vertices.Select(i => points[i]).Append(site.Point)).ToList(),
|
|
||||||
};
|
roads = generator.Delaunay.Morph(s => s.Point, e => Point.Dist(generator.Delaunay.Vertices[e.Item1].Point, generator.Delaunay.Vertices[e.Item2].Point));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class GraphGeneratorDebug
|
public class GraphGeneratorDebug
|
||||||
{
|
{
|
||||||
@ -59,20 +81,125 @@ namespace Assets.Scripts
|
|||||||
public bool displayEdges = false;
|
public bool displayEdges = false;
|
||||||
public bool displayCities = true;
|
public bool displayCities = true;
|
||||||
public bool displayCityRoads = true;
|
public bool displayCityRoads = true;
|
||||||
|
public bool displayCityMainRoads = true;
|
||||||
|
public bool displayWorldRoads = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireComponent(typeof(GraphGenerator))]
|
[RequireComponent(typeof(GraphGenerator))]
|
||||||
public class CityGenerator : MonoBehaviour
|
public class CityGenerator : MonoBehaviour
|
||||||
{
|
{
|
||||||
private System.Random _random;
|
private System.Random _random;
|
||||||
private Graph<Point> _voronoiGraph;
|
private Graph<Point> _voronoiGraph;
|
||||||
private Graph<Location> _locationGraph;
|
private Graph<Location> _locationGraph;
|
||||||
private Graph<Voronoi.Site> _basicGraph;
|
private Graph<Site> _basicGraph;
|
||||||
private List<int> verticesForRoads;
|
private List<int> verticesForRoads;
|
||||||
private List<int> verticesForCitites;
|
private List<int> verticesForCitites;
|
||||||
public GraphGeneratorDebug debug;
|
public GraphGeneratorDebug debug;
|
||||||
public City city;
|
public City city;
|
||||||
public List<City> cities = new List<City>();
|
public List<City> cities = new List<City>();
|
||||||
|
Graph<Site, double> roads = new Graph<Site, double>();
|
||||||
|
Graph<Point> mainRoads = new Graph<Point>();
|
||||||
|
|
||||||
|
List<int> CountProbablitityOfCity(List<int> vertices)
|
||||||
|
{
|
||||||
|
List<int> probabilities = new List<int>();
|
||||||
|
vertices.ForEach(v => probabilities.Add((v % 2 == 0 ? _random.Next(0, 50) : _random.Next(50, 100))));
|
||||||
|
return probabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> LocateCities()
|
||||||
|
{
|
||||||
|
// var verticesForCitites = CountProbablitityOfCity(ChoosePoints(20));
|
||||||
|
// verticesForCitites.Sort();
|
||||||
|
// return verticesForCitites.GetRange(0, 10);
|
||||||
|
return ChoosePoints(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
City CreateCity(int vertex, int size)
|
||||||
|
{
|
||||||
|
City newCity = new City(_basicGraph);
|
||||||
|
var site = _basicGraph.Vertices[vertex];
|
||||||
|
var sites = new List<Site>() { site };
|
||||||
|
var location = site.Location;
|
||||||
|
|
||||||
|
for (int i = 0; i < size - 1; i++)
|
||||||
|
{
|
||||||
|
location = site.Location;
|
||||||
|
var neighbours = _basicGraph.Neighbours(site.Index).Select(j => _basicGraph.Vertices[j]).Where(s => s.Location == location).ToList();
|
||||||
|
site = neighbours[_random.Next(neighbours.Count)];
|
||||||
|
sites.Add(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var s in sites.Distinct())
|
||||||
|
{
|
||||||
|
newCity.AddSite(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
newCity.CreateRoadGraph(_voronoiGraph.Vertices);
|
||||||
|
newCity.mainRoad = ConnectPointsIntoRoads(newCity.roads);
|
||||||
|
return newCity;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> ChoosePoints(int number)
|
||||||
|
{
|
||||||
|
var vertices = new List<int>();
|
||||||
|
for (int i = 0; i < number; i++)
|
||||||
|
{
|
||||||
|
var randomLocation = _locationGraph.Vertices[_random.Next(1, _locationGraph.Vertices.Count())];
|
||||||
|
var count = randomLocation.Sites.Count();
|
||||||
|
Site randomPoint;
|
||||||
|
randomPoint = randomLocation.Sites[_random.Next(0, count)];
|
||||||
|
vertices.Add(randomPoint.Index);
|
||||||
|
}
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
Graph<Point> ConnectPointsIntoRoads(Graph<Point, double> graph)
|
||||||
|
{
|
||||||
|
|
||||||
|
//1.Sort all the edges in non - decreasing order of their weight.
|
||||||
|
//2.Pick the smallest edge.Check if it forms a cycle with the spanning tree formed so far. If cycle is not formed, include this edge.Else, discard it.
|
||||||
|
//3.Repeat step#2 until there are (V-1) edges in the spanning tree.
|
||||||
|
|
||||||
|
graph.Edges = graph.Edges.OrderBy(e => graph.GetEdgeData(e));
|
||||||
|
graph._edgeData = graph._edgeData.OrderBy(e => e.Value).ToDictionary(e => e.Key, k => k.Value);
|
||||||
|
|
||||||
|
Graph<Point> spanningTree = new Graph<Point>();
|
||||||
|
spanningTree.Vertices = graph.Vertices;
|
||||||
|
|
||||||
|
foreach (var edge in graph.Edges)
|
||||||
|
{
|
||||||
|
spanningTree.AddEdge(edge.Item1, edge.Item2);
|
||||||
|
if (Graph<Point>.HasCycle(spanningTree))
|
||||||
|
spanningTree.DeleteEdge(edge.Item1, edge.Item2);
|
||||||
|
if (spanningTree.Edges.Count() == spanningTree.Vertices.Count() - 1)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return spanningTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateWorldRoadGraph()
|
||||||
|
{
|
||||||
|
roads.Vertices = verticesForCitites.Select(i => _basicGraph.Vertices[i]).ToList();
|
||||||
|
|
||||||
|
VoronoiGenerator generator = new VoronoiGenerator(roads.Vertices.Select(s => s.Point).ToList());
|
||||||
|
generator.Generate();
|
||||||
|
|
||||||
|
roads.Edges = generator.Delaunay.Edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void AddLanesToRoads()
|
||||||
|
{
|
||||||
|
// ergo dodaj randomowe połączenia xd
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// UI Methods
|
||||||
|
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
Reset();
|
Reset();
|
||||||
@ -89,10 +216,8 @@ namespace Assets.Scripts
|
|||||||
public void NewCity()
|
public void NewCity()
|
||||||
{
|
{
|
||||||
cities.Add(CreateCity(LocateCities()[_random.Next(0, 10)], _random.Next(0, 10)));
|
cities.Add(CreateCity(LocateCities()[_random.Next(0, 10)], _random.Next(0, 10)));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Generate()
|
public void Generate()
|
||||||
{
|
{
|
||||||
var graphGenerator = GetComponent<GraphGenerator>();
|
var graphGenerator = GetComponent<GraphGenerator>();
|
||||||
@ -106,7 +231,12 @@ namespace Assets.Scripts
|
|||||||
{
|
{
|
||||||
cities.Add(CreateCity(index, _random.Next(0, 10)));
|
cities.Add(CreateCity(index, _random.Next(0, 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateWorldRoadGraph();
|
||||||
|
mainRoads = roads.Morph(s => s.Point);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisplayGraphVertices(Graph<Point> graph)
|
private void DisplayGraphVertices(Graph<Point> graph)
|
||||||
{
|
{
|
||||||
var vertices = graph.Vertices.Select(p => p.ToVector3());
|
var vertices = graph.Vertices.Select(p => p.ToVector3());
|
||||||
@ -115,7 +245,6 @@ namespace Assets.Scripts
|
|||||||
foreach (var (v, i) in vertices.Select((x, i) => (x, i)))
|
foreach (var (v, i) in vertices.Select((x, i) => (x, i)))
|
||||||
{
|
{
|
||||||
Gizmos.DrawSphere(v, 1);
|
Gizmos.DrawSphere(v, 1);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,92 +312,27 @@ namespace Assets.Scripts
|
|||||||
{
|
{
|
||||||
Gizmos.color = Color.blue;
|
Gizmos.color = Color.blue;
|
||||||
foreach (var city in cities)
|
foreach (var city in cities)
|
||||||
|
{
|
||||||
|
DisplayGraphEdges(city.roads);
|
||||||
DisplayGraphVertices(city.roads);
|
DisplayGraphVertices(city.roads);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
List<int> CountProbablitityOfCity(List<int> vertices)
|
if (debug.displayCityMainRoads)
|
||||||
{
|
|
||||||
List<int> probabilities = new List<int>();
|
|
||||||
vertices.ForEach(v => probabilities.Add((v % 2 == 0 ? _random.Next(0, 50) : _random.Next(50, 100))));
|
|
||||||
return probabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int> LocateCities()
|
|
||||||
{
|
|
||||||
// var verticesForCitites = CountProbablitityOfCity(ChoosePoints(20));
|
|
||||||
// verticesForCitites.Sort();
|
|
||||||
// return verticesForCitites.GetRange(0, 10);
|
|
||||||
return ChoosePoints(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
City CreateCity(int vertex, int size)
|
|
||||||
{
|
|
||||||
City newCity = new City(_basicGraph);
|
|
||||||
var site = _basicGraph.Vertices[vertex];
|
|
||||||
var sites = new List<Site>();
|
|
||||||
var location = site.Location;
|
|
||||||
|
|
||||||
for (int i = 0; i < size - 1; i++)
|
|
||||||
{
|
{
|
||||||
location = site.Location;
|
Gizmos.color = new Color(2.54f, 0.127f, 1.56f);
|
||||||
var neighbours = _basicGraph.Neighbours(site.Index).Select(j => _basicGraph.Vertices[j]).Where(s => s.Location == location).ToList();
|
//Gizmos.color = Color.green;
|
||||||
site = neighbours[_random.Next(neighbours.Count)];
|
foreach (var city in cities)
|
||||||
sites.Add(site);
|
{
|
||||||
|
DisplayGraphEdges(city.mainRoad);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (debug.displayWorldRoads)
|
||||||
foreach (var s in sites.Distinct())
|
|
||||||
{
|
{
|
||||||
newCity.AddSite(s);
|
// Gizmos.color = new Color(0.254f, 0.127f, 0.156f);
|
||||||
|
Gizmos.color = Color.blue;
|
||||||
|
DisplayGraphEdges(mainRoads);
|
||||||
}
|
}
|
||||||
|
|
||||||
newCity.CreateRoadGraph(_voronoiGraph.Vertices);
|
|
||||||
|
|
||||||
return newCity;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int> ChoosePoints(int number)
|
|
||||||
{
|
|
||||||
var vertices = new List<int>();
|
|
||||||
for (int i = 0; i < number; i++)
|
|
||||||
{
|
|
||||||
var randomLocation = _locationGraph.Vertices[_random.Next(1, _locationGraph.Vertices.Count())];
|
|
||||||
var count = randomLocation.Sites.Count();
|
|
||||||
Site randomPoint;
|
|
||||||
randomPoint = randomLocation.Sites[_random.Next(0, count)];
|
|
||||||
vertices.Add(randomPoint.Index);
|
|
||||||
}
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
List<int> ChoosePoints(int number, Graph<Point> graph)
|
|
||||||
{
|
|
||||||
|
|
||||||
var vertices = new List<int>();
|
|
||||||
for (int i = 0; i < number; i++)
|
|
||||||
{
|
|
||||||
var randomLocation = _locationGraph.Vertices[_random.Next(1, _locationGraph.Vertices.Count())];
|
|
||||||
var count = randomLocation.Sites.Count();
|
|
||||||
Site randomPoint;
|
|
||||||
randomPoint = randomLocation.Sites[_random.Next(0, count)];
|
|
||||||
vertices.Add(randomPoint.Index);
|
|
||||||
}
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void ConnectPointIntoRoads(/*punkty*/)
|
|
||||||
{
|
|
||||||
// spanning tree
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
void AddLanesToRoads()
|
|
||||||
{
|
|
||||||
// ergo dodaj randomowe połączenia xd
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user