mirror of
https://github.com/Outer-Wilds-New-Horizons/new-horizons.git
synced 2025-12-11 20:15:44 +01:00
feat: added full config file updating, added menu items for this as well
This commit is contained in:
parent
27247d79a1
commit
7011b6ec3a
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using NewHorizons.External.Configs;
|
||||||
|
using OWML.Common;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -22,9 +24,16 @@ namespace NewHorizons.Utility
|
|||||||
// menu params
|
// menu params
|
||||||
private Vector2 recentPropsScrollPosition = Vector2.zero;
|
private Vector2 recentPropsScrollPosition = Vector2.zero;
|
||||||
private HashSet<string> favoriteProps = new HashSet<string>();
|
private HashSet<string> favoriteProps = new HashSet<string>();
|
||||||
private char separatorCharacter = '☧'; // since no chars are illegal in game object names, I picked one that's extremely unlikely to be used to be a separator
|
public static readonly char separatorCharacter = '☧'; // since no chars are illegal in game object names, I picked one that's extremely unlikely to be used to be a separator
|
||||||
private string favoritePropsPlayerPrefKey = "FavoriteProps";
|
private string favoritePropsPlayerPrefKey = "FavoriteProps";
|
||||||
|
|
||||||
|
//private string workingModName = "";
|
||||||
|
private IModBehaviour loadedMod = null;
|
||||||
|
private Dictionary<string, IPlanetConfig> loadedConfigFiles = new Dictionary<string, IPlanetConfig>();
|
||||||
|
private bool saveButtonUnlocked = false;
|
||||||
|
private bool propsHaveBeenLoaded = false;
|
||||||
|
private Vector2 recentModListScrollPosition = Vector2.zero;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_dpp = this.GetRequiredComponent<DebugPropPlacer>();
|
_dpp = this.GetRequiredComponent<DebugPropPlacer>();
|
||||||
@ -113,6 +122,89 @@ namespace NewHorizons.Utility
|
|||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
|
|
||||||
|
GUILayout.Space(5);
|
||||||
|
|
||||||
|
// continue working on existing mod
|
||||||
|
|
||||||
|
GUILayout.Label("Name of your mod");
|
||||||
|
if (loadedMod == null)
|
||||||
|
{
|
||||||
|
recentModListScrollPosition = GUILayout.BeginScrollView(recentModListScrollPosition, GUILayout.Width(EditorMenuSize.x), GUILayout.Height(100));
|
||||||
|
|
||||||
|
foreach (var mod in Main.MountedAddons)
|
||||||
|
{
|
||||||
|
if (GUILayout.Button(mod.ModHelper.Manifest.UniqueName))
|
||||||
|
{
|
||||||
|
loadedMod = mod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GUILayout.Label(loadedMod.ModHelper.Manifest.UniqueName);
|
||||||
|
}
|
||||||
|
// workingModName = GUILayout.TextField(workingModName);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GUI.enabled = !propsHaveBeenLoaded && loadedMod != null;
|
||||||
|
if (GUILayout.Button("Load Detail Props from Configs", GUILayout.ExpandWidth(false)))
|
||||||
|
{
|
||||||
|
propsHaveBeenLoaded = true;
|
||||||
|
var folder = loadedMod.ModHelper.Manifest.ModFolderPath;
|
||||||
|
|
||||||
|
if (System.IO.Directory.Exists(folder + "planets"))
|
||||||
|
{
|
||||||
|
foreach (var file in System.IO.Directory.GetFiles(folder + @"planets\", "*.json", System.IO.SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
Logger.Log("READING FROM CONFIG @ " + file);
|
||||||
|
var relativeDirectory = file.Replace(folder, "");
|
||||||
|
var bodyConfig = loadedMod.ModHelper.Storage.Load<PlanetConfig>(relativeDirectory);
|
||||||
|
loadedConfigFiles[file] = bodyConfig;
|
||||||
|
_dpp.FindAndRegisterPropsFromConfig(bodyConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GUI.enabled = true;
|
||||||
|
|
||||||
|
GUILayout.Space(5);
|
||||||
|
|
||||||
|
{
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
if (GUILayout.Button(saveButtonUnlocked ? " O " : " | ", GUILayout.ExpandWidth(false)))
|
||||||
|
{
|
||||||
|
saveButtonUnlocked = !saveButtonUnlocked;
|
||||||
|
}
|
||||||
|
GUI.enabled = saveButtonUnlocked;
|
||||||
|
if (GUILayout.Button("Update your mod's configs"))
|
||||||
|
{
|
||||||
|
UpdateLoadedConfigs();
|
||||||
|
|
||||||
|
foreach (var filePath in loadedConfigFiles.Keys)
|
||||||
|
{
|
||||||
|
Logger.Log("Saving... " + filePath);
|
||||||
|
Main.Instance.ModHelper.Storage.Save<IPlanetConfig>(loadedConfigFiles[filePath], filePath);
|
||||||
|
}
|
||||||
|
saveButtonUnlocked = false;
|
||||||
|
}
|
||||||
|
GUI.enabled = true;
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("Print your mod's updated configs"))
|
||||||
|
{
|
||||||
|
UpdateLoadedConfigs();
|
||||||
|
|
||||||
|
foreach (var filePath in loadedConfigFiles.Keys)
|
||||||
|
{
|
||||||
|
Logger.Log("Updated copy of " + filePath);
|
||||||
|
Logger.Log(Newtonsoft.Json.JsonConvert.SerializeObject(loadedConfigFiles[filePath], Newtonsoft.Json.Formatting.Indented));
|
||||||
|
}
|
||||||
|
//_dpp.PrintConfigs();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: field to provide name of mod to load configs from, plus button to load those into the PropPlaecr (make sure not to load more than once, once the button has been pushed, disable it)
|
// TODO: field to provide name of mod to load configs from, plus button to load those into the PropPlaecr (make sure not to load more than once, once the button has been pushed, disable it)
|
||||||
// TODO: add a warning that the button cannot be pushed more than once
|
// TODO: add a warning that the button cannot be pushed more than once
|
||||||
|
|
||||||
@ -122,6 +214,40 @@ namespace NewHorizons.Utility
|
|||||||
GUILayout.EndArea();
|
GUILayout.EndArea();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateLoadedConfigs()
|
||||||
|
{
|
||||||
|
// for each keyvalue in _dpp.GetPropsConfigByBody()
|
||||||
|
// find the matching entry loadedConfigFiles
|
||||||
|
// entry matches if the value of AstroOBjectLocator.FindBody(key) matches
|
||||||
|
|
||||||
|
var newDetails = _dpp.GetPropsConfigByBody(true);
|
||||||
|
|
||||||
|
|
||||||
|
//var allConfigsForMod = Main.Instance.BodyDict[Main.CurrentStarSystem].Where(x => x.Mod == mod).Select(x => x.Config)
|
||||||
|
|
||||||
|
//var allConfigs = Main.BodyDict.Values.SelectMany(x => x).Where(x => x.Mod == loadedMod).Select(x => x.Config);
|
||||||
|
|
||||||
|
// Get all configs
|
||||||
|
foreach (var filePath in loadedConfigFiles.Keys)
|
||||||
|
{
|
||||||
|
if (loadedConfigFiles[filePath].Name == null || AstroObjectLocator.GetAstroObject(loadedConfigFiles[filePath].Name) == null) continue;
|
||||||
|
|
||||||
|
var bodyName = loadedConfigFiles[filePath].Name;
|
||||||
|
var astroObjectName = AstroObjectLocator.GetAstroObject(bodyName).name;
|
||||||
|
var systemName = loadedConfigFiles[filePath].StarSystem;
|
||||||
|
var composedName = systemName + separatorCharacter + astroObjectName;
|
||||||
|
|
||||||
|
if (!newDetails.ContainsKey(composedName)) continue;
|
||||||
|
|
||||||
|
if (loadedConfigFiles[filePath].Props == null)
|
||||||
|
{
|
||||||
|
(loadedConfigFiles[filePath] as PlanetConfig).Props = new External.PropModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedConfigFiles[filePath].Props.Details = newDetails[composedName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitMenu()
|
private void InitMenu()
|
||||||
{
|
{
|
||||||
if (_editorMenuStyle != null) return;
|
if (_editorMenuStyle != null) return;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
|
using static NewHorizons.External.PropModule;
|
||||||
|
|
||||||
namespace NewHorizons.Utility
|
namespace NewHorizons.Utility
|
||||||
{
|
{
|
||||||
@ -17,12 +18,16 @@ namespace NewHorizons.Utility
|
|||||||
private struct PropPlacementData
|
private struct PropPlacementData
|
||||||
{
|
{
|
||||||
public string body;
|
public string body;
|
||||||
|
public string system;
|
||||||
|
|
||||||
public string propPath;
|
public string propPath;
|
||||||
|
|
||||||
public GameObject gameObject;
|
public GameObject gameObject;
|
||||||
public Vector3 pos { get { return gameObject.transform.localPosition; } }
|
public Vector3 pos { get { return gameObject.transform.localPosition; } }
|
||||||
public Vector3 rotation { get { return gameObject.transform.localEulerAngles; } }
|
public Vector3 rotation { get { return gameObject.transform.localEulerAngles; } }
|
||||||
|
|
||||||
|
public string assetBundle;
|
||||||
|
public string[] removeChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_1/Props_DreamZone_1/OtherComponentsGroup/Trees_Z1/DreamHouseIsland/Tree_DW_M_Var
|
// DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_1/Props_DreamZone_1/OtherComponentsGroup/Trees_Z1/DreamHouseIsland/Tree_DW_M_Var
|
||||||
@ -34,7 +39,7 @@ namespace NewHorizons.Utility
|
|||||||
private List<PropPlacementData> deletedProps = new List<PropPlacementData>();
|
private List<PropPlacementData> deletedProps = new List<PropPlacementData>();
|
||||||
private DebugRaycaster _rc;
|
private DebugRaycaster _rc;
|
||||||
|
|
||||||
public List<string> RecentlyPlacedProps = new List<string>();
|
public HashSet<string> RecentlyPlacedProps = new HashSet<string>();
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@ -51,10 +56,10 @@ namespace NewHorizons.Utility
|
|||||||
PlaceObject();
|
PlaceObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Keyboard.current[Key.Semicolon].wasReleasedThisFrame)
|
//if (Keyboard.current[Key.Semicolon].wasReleasedThisFrame)
|
||||||
{
|
//{
|
||||||
PrintConfigs();
|
// PrintConfigs();
|
||||||
}
|
//}
|
||||||
|
|
||||||
if (Keyboard.current[Key.Minus].wasReleasedThisFrame)
|
if (Keyboard.current[Key.Minus].wasReleasedThisFrame)
|
||||||
{
|
{
|
||||||
@ -133,7 +138,7 @@ namespace NewHorizons.Utility
|
|||||||
prop.transform.parent = g.transform.parent;
|
prop.transform.parent = g.transform.parent;
|
||||||
GameObject.Destroy(g);
|
GameObject.Destroy(g);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch
|
||||||
{
|
{
|
||||||
Logger.Log($"Failed to place object {currentObject} on body ${data.hitObject} at location ${data.pos}.");
|
Logger.Log($"Failed to place object {currentObject} on body ${data.hitObject} at location ${data.pos}.");
|
||||||
}
|
}
|
||||||
@ -143,16 +148,38 @@ namespace NewHorizons.Utility
|
|||||||
{
|
{
|
||||||
AstroObject planet = AstroObjectLocator.GetAstroObject(config.Name);
|
AstroObject planet = AstroObjectLocator.GetAstroObject(config.Name);
|
||||||
|
|
||||||
|
if (planet == null || planet.GetRootSector() == null) return;
|
||||||
|
if (config.Props == null || config.Props.Details == null) return;
|
||||||
|
|
||||||
|
List<Transform> potentialProps = new List<Transform>();
|
||||||
|
foreach (Transform child in planet.GetRootSector().transform) potentialProps.Add(child);
|
||||||
|
potentialProps.Where(potentialProp => potentialProp.gameObject.name.EndsWith("(Clone)")).ToList();
|
||||||
|
|
||||||
foreach (var detail in config.Props.Details)
|
foreach (var detail in config.Props.Details)
|
||||||
{
|
{
|
||||||
foreach (Transform child in planet.GetRootSector().transform)
|
var propPathElements = detail.path.Split('/');
|
||||||
{
|
string propName = propPathElements[propPathElements.Length-1];
|
||||||
bool childMatchesDetail = false; // TODO: this
|
|
||||||
|
|
||||||
if (childMatchesDetail)
|
potentialProps
|
||||||
|
.Where(potentialProp => potentialProp.gameObject.name == propName+"(Clone)")
|
||||||
|
.OrderBy(potentialProp => Vector3.Distance(potentialProp.localPosition, detail.position))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (potentialProps.Count <= 0)
|
||||||
{
|
{
|
||||||
RegisterProp(detail.path, child.gameObject);
|
Logger.LogError($"No candidate found for prop {detail.path} on planet ${config.Name}.");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Transform spawnedProp = potentialProps[0];
|
||||||
|
PropPlacementData data = RegisterProp_WithReturn(config.Name, spawnedProp.gameObject, detail.path);
|
||||||
|
data.assetBundle = detail.assetBundle;
|
||||||
|
data.removeChildren = detail.removeChildren;
|
||||||
|
potentialProps.Remove(spawnedProp);
|
||||||
|
|
||||||
|
if (!RecentlyPlacedProps.Contains(data.propPath))
|
||||||
|
{
|
||||||
|
RecentlyPlacedProps.Add(data.propPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,72 +189,119 @@ namespace NewHorizons.Utility
|
|||||||
RegisterProp_WithReturn(bodyGameObjectName, prop);
|
RegisterProp_WithReturn(bodyGameObjectName, prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PropPlacementData RegisterProp_WithReturn(string bodyGameObjectName, GameObject prop)
|
private PropPlacementData RegisterProp_WithReturn(string bodyGameObjectName, GameObject prop, string propPath = null, string systemName = null)
|
||||||
{
|
{
|
||||||
if (Main.Debug)
|
if (Main.Debug)
|
||||||
{
|
{
|
||||||
// TOOD: make this prop an item
|
// TOOD: make this prop an item
|
||||||
}
|
}
|
||||||
|
|
||||||
string bodyName = bodyGameObjectName.Substring(0, bodyGameObjectName.Length-"_Body".Length);
|
string bodyName = bodyGameObjectName.EndsWith("_Body")
|
||||||
|
? bodyGameObjectName.Substring(0, bodyGameObjectName.Length-"_Body".Length)
|
||||||
|
: bodyGameObjectName;
|
||||||
PropPlacementData data = new PropPlacementData
|
PropPlacementData data = new PropPlacementData
|
||||||
{
|
{
|
||||||
body = bodyName,
|
body = bodyName,
|
||||||
propPath = currentObject,
|
propPath = propPath == null ? currentObject : propPath,
|
||||||
gameObject = prop
|
gameObject = prop,
|
||||||
|
system = systemName
|
||||||
};
|
};
|
||||||
|
|
||||||
props.Add(data);
|
props.Add(data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PrintConfigs()
|
//public void PrintConfigs()
|
||||||
{
|
//{
|
||||||
foreach(string configFile in GenerateConfigs())
|
// foreach(string configFile in GenerateConfigs())
|
||||||
{
|
// {
|
||||||
Logger.Log(configFile);
|
// Logger.Log(configFile);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
public List<String> GenerateConfigs()
|
//public List<String> GenerateConfigs()
|
||||||
|
//{
|
||||||
|
// var groupedProps = props
|
||||||
|
// .GroupBy(p => AstroObjectLocator.GetAstroObject(p.body).name)
|
||||||
|
// .Select(grp => grp.ToList())
|
||||||
|
// .ToList();
|
||||||
|
|
||||||
|
// List<string> configFiles = new List<string>();
|
||||||
|
|
||||||
|
// foreach (List<PropPlacementData> bodyProps in groupedProps)
|
||||||
|
// {
|
||||||
|
// string configFile =
|
||||||
|
// "{" + Environment.NewLine +
|
||||||
|
// " \"$schema\": \"https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/master/NewHorizons/schema.json\"," + Environment.NewLine +
|
||||||
|
// $" \"name\" : \"{bodyProps[0].body}\"," + Environment.NewLine +
|
||||||
|
// " \"Props\" :" + Environment.NewLine +
|
||||||
|
// " {" + Environment.NewLine +
|
||||||
|
// " \"details\": [" + Environment.NewLine;
|
||||||
|
|
||||||
|
// for(int i = 0; i < bodyProps.Count; i++)
|
||||||
|
// {
|
||||||
|
// PropPlacementData prop = bodyProps[i];
|
||||||
|
|
||||||
|
// string positionString = $"\"x\":{prop.pos.x},\"y\":{prop.pos.y},\"z\":{prop.pos.z}";
|
||||||
|
// string rotationString = $"\"x\":{prop.rotation.x},\"y\":{prop.rotation.y},\"z\":{prop.rotation.z}";
|
||||||
|
// string endingString = i == bodyProps.Count-1 ? "" : ",";
|
||||||
|
|
||||||
|
// configFile += " {" +
|
||||||
|
// "\"path\" : \"" +prop.propPath+ "\", " +
|
||||||
|
// "\"position\": {"+positionString+"}, " +
|
||||||
|
// "\"rotation\": {"+rotationString+"}, " +
|
||||||
|
// "\"scale\": 1"+
|
||||||
|
// (prop.assetBundle == null ? "" : $", \"assetBundle\": \"{prop.assetBundle}\"") +
|
||||||
|
// (prop.removeChildren == null ? "" : $", \"removeChildren\": \"[{string.Join(",",prop.removeChildren)}]\"") +
|
||||||
|
// "}" + endingString + Environment.NewLine;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// configFile +=
|
||||||
|
// " ]" + Environment.NewLine +
|
||||||
|
// " }" + Environment.NewLine +
|
||||||
|
// "}";
|
||||||
|
|
||||||
|
// configFiles.Add(configFile);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return configFiles;
|
||||||
|
//}
|
||||||
|
|
||||||
|
public Dictionary<string, DetailInfo[]> GetPropsConfigByBody(bool useAstroObjectName = false)
|
||||||
{
|
{
|
||||||
var groupedProps = props
|
var groupedProps = props
|
||||||
.GroupBy(p => p.body)
|
.GroupBy(p => p.system + "." + p.body)
|
||||||
.Select(grp => grp.ToList())
|
.Select(grp => grp.ToList())
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
List<string> configFiles = new List<string>();
|
Dictionary<string, DetailInfo[]> propConfigs = new Dictionary<string, DetailInfo[]>();
|
||||||
|
|
||||||
foreach (List<PropPlacementData> bodyProps in groupedProps)
|
foreach (List<PropPlacementData> bodyProps in groupedProps)
|
||||||
{
|
{
|
||||||
string configFile =
|
|
||||||
"{" + Environment.NewLine +
|
if (bodyProps == null || bodyProps.Count == 0) continue;
|
||||||
" \"$schema\": \"https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/master/NewHorizons/schema.json\"," + Environment.NewLine +
|
if ( AstroObjectLocator.GetAstroObject(bodyProps[0].body) == null ) continue;
|
||||||
$" \"name\" : \"{bodyProps[0].body}\"," + Environment.NewLine +
|
string bodyName = useAstroObjectName ? AstroObjectLocator.GetAstroObject(bodyProps[0].body).name : bodyProps[0].body;
|
||||||
" \"Props\" :" + Environment.NewLine +
|
|
||||||
" {" + Environment.NewLine +
|
DetailInfo[] infoArray = new DetailInfo[bodyProps.Count];
|
||||||
" \"details\": [" + Environment.NewLine;
|
propConfigs[bodyProps[0].system + DebugMenu.separatorCharacter + bodyName] = infoArray;
|
||||||
|
|
||||||
for(int i = 0; i < bodyProps.Count; i++)
|
for(int i = 0; i < bodyProps.Count; i++)
|
||||||
{
|
{
|
||||||
PropPlacementData prop = bodyProps[i];
|
infoArray[i] = new DetailInfo()
|
||||||
|
{
|
||||||
string positionString = $"\"x\":{prop.pos.x},\"y\":{prop.pos.y},\"z\":{prop.pos.z}";
|
path = bodyProps[i].propPath,
|
||||||
string rotationString = $"\"x\":{prop.rotation.x},\"y\":{prop.rotation.y},\"z\":{prop.rotation.z}";
|
assetBundle = bodyProps[i].assetBundle,
|
||||||
string endingString = i == bodyProps.Count-1 ? "" : ",";
|
position = bodyProps[i].gameObject.transform.localPosition,
|
||||||
|
rotation = bodyProps[i].gameObject.transform.localEulerAngles,
|
||||||
configFile += " {\"path\" : \"" +prop.propPath+ "\", \"position\": {"+positionString+"}, \"rotation\": {"+rotationString+"}, \"scale\": 1}" + endingString + Environment.NewLine;
|
scale = bodyProps[i].gameObject.transform.localScale.x,
|
||||||
|
//public bool alignToNormal; // TODO: figure out how to recover this (or actually, rotation should cover it)
|
||||||
|
removeChildren = bodyProps[i].removeChildren
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile +=
|
return propConfigs;
|
||||||
" ]" + Environment.NewLine +
|
|
||||||
" }" + Environment.NewLine +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
configFiles.Add(configFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return configFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteLast()
|
public void DeleteLast()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user