Add A25 to A26 scripts
This commit is contained in:
parent
0af8f6b051
commit
675591324c
0
.normalized_ao_public
Normal file
0
.normalized_ao_public
Normal file
68
A25ToA26.py
Normal file
68
A25ToA26.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from A25_A26.ElevationBonusFixer import ElevationBonusFixer
|
||||||
|
from A25_A26.PlayerXMLFixer import PlayerXMLFixer
|
||||||
|
from A25_A26.SessionIconPathFixer import SessionIconPathFixer
|
||||||
|
from A25_A26.FixNewPlayerParent import FixNewPlayerParent
|
||||||
|
from A25_A26.FormationFixer import FormationFixer
|
||||||
|
from A25_A26.PlayerXMLFormationFixer import PlayerXMLFormationFixer
|
||||||
|
from A25_A26.P256 import TemplateFixer, ModifiersFixer
|
||||||
|
from A25_A26.P259 import convert_recursively
|
||||||
|
from A25_A26.P261 import TemplateFixer as TemplateFixerP261, SedLike
|
||||||
|
from A25_A26.FancyGrassRemover import FancyGrassRemover
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = ArgumentParser(description='A25 to A26 converter.')
|
||||||
|
parser.add_argument('-r', '--root', action='store', dest='root', default=os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
parser.add_argument('-m', '--mod', action='store', dest='mod', default='public')
|
||||||
|
parser.add_argument('-v', '--verbose', action='store_true', default=False, help="Be verbose.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
script_dir = args.root
|
||||||
|
mod_name = args.mod
|
||||||
|
path = Path(script_dir) / mod_name
|
||||||
|
print(f"Running in {path}")
|
||||||
|
print("Running P256...")
|
||||||
|
template_fixer = TemplateFixer(path)
|
||||||
|
template_fixer.run()
|
||||||
|
modifier_fixer = ModifiersFixer(path)
|
||||||
|
modifier_fixer.run()
|
||||||
|
if not os.path.exists('.normalized_ao_' + mod_name) and os.path.isdir(path):
|
||||||
|
print("Running P259...")
|
||||||
|
convert_recursively(path)
|
||||||
|
(Path('.normalized_ao_' + mod_name)).touch()
|
||||||
|
else:
|
||||||
|
print("Skipping P259...")
|
||||||
|
print("Fixing r26074...")
|
||||||
|
elevationFixer = ElevationBonusFixer(path, args.verbose)
|
||||||
|
elevationFixer.run()
|
||||||
|
print("Running P261...")
|
||||||
|
template_fixer = TemplateFixerP261(path)
|
||||||
|
template_fixer.run()
|
||||||
|
print("Running FancyGrassRemover...")
|
||||||
|
remover = FancyGrassRemover(path, args.verbose)
|
||||||
|
remover.run()
|
||||||
|
print("Fixing r26298...")
|
||||||
|
fixer = PlayerXMLFixer(path, args.verbose)
|
||||||
|
fixer.run()
|
||||||
|
print("Fixing r26299...")
|
||||||
|
fixer = PlayerXMLFormationFixer(path, args.verbose)
|
||||||
|
fixer.run()
|
||||||
|
print("Fixing r26317...")
|
||||||
|
fixer = SessionIconPathFixer(path, args.verbose)
|
||||||
|
fixer.run()
|
||||||
|
print("Fixing r26458 ...")
|
||||||
|
fixer = FixNewPlayerParent(path, args.verbose)
|
||||||
|
fixer.run()
|
||||||
|
print("Fixing r26476...")
|
||||||
|
fixer = FormationFixer(path, args.verbose)
|
||||||
|
fixer.run()
|
||||||
|
|
54
A25_A26/ElevationBonusFixer.py
Normal file
54
A25_A26/ElevationBonusFixer.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from utils.fixers.BaseFixer import BaseFixer
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import os
|
||||||
|
|
||||||
|
class ElevationBonusFixer(BaseFixer):
|
||||||
|
def __init__(self, vfs_root, verbose=False):
|
||||||
|
BaseFixer.__init__(self, vfs_root, verbose, __name__)
|
||||||
|
self.add_files(os.path.join('simulation', 'templates'), tuple(".xml"))
|
||||||
|
|
||||||
|
def fixEffectDelay(self, root):
|
||||||
|
cmpAttack = root.find('Attack')
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if cmpAttack is not None:
|
||||||
|
for attack_type in cmpAttack:
|
||||||
|
delay = attack_type.find('Delay')
|
||||||
|
if delay is not None:
|
||||||
|
delay.tag = 'EffectDelay'
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def fixElevationBonus(self, root):
|
||||||
|
cmpAttack = root.find('Attack')
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if cmpAttack is not None:
|
||||||
|
for attack_type in cmpAttack:
|
||||||
|
bonus = attack_type.find('ElevationBonus')
|
||||||
|
if bonus is not None:
|
||||||
|
ET.SubElement(bonus, 'X').text = '0'
|
||||||
|
ET.SubElement(bonus, 'Y').text = bonus.text
|
||||||
|
ET.SubElement(bonus, 'Z').text = '0'
|
||||||
|
bonus.tag = 'Origin'
|
||||||
|
bonus.text = ''
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for file in self.files:
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
if self.fixElevationBonus(root) or self.fixEffectDelay(root):
|
||||||
|
self.logger.info(f'Saving {file}')
|
||||||
|
self.save_xml_file(tree, root, file)
|
43
A25_A26/FancyGrassRemover.py
Normal file
43
A25_A26/FancyGrassRemover.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from utils.fixers.BaseFixer import BaseFixer
|
||||||
|
from utils.PMPMap import PmpMap
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class FancyGrassRemover(BaseFixer):
|
||||||
|
def __init__(self, vfs_root, verbose=False):
|
||||||
|
BaseFixer.__init__(self, vfs_root, verbose, __name__)
|
||||||
|
self.vfs_root = Path(vfs_root)
|
||||||
|
self.verbose = verbose
|
||||||
|
self.add_files(os.path.join("maps"), tuple(".pmp"))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for filePath in self.files:
|
||||||
|
with open(filePath, "r+b") as f1:
|
||||||
|
pmpMap = PmpMap(f1)
|
||||||
|
hasChanged = False
|
||||||
|
if (self.verbose):
|
||||||
|
self.logger.info(f'Parsing {filePath}')
|
||||||
|
for textureIndex in range(0, len(pmpMap.textures)):
|
||||||
|
texture = pmpMap.textures[textureIndex]
|
||||||
|
if '_fancy' in texture:
|
||||||
|
pmpMap.header.data_size -= len(texture)
|
||||||
|
if (self.verbose):
|
||||||
|
self.logger.info(f'Replacing {texture} by {texture.replace("_fancy", "")}')
|
||||||
|
pmpMap.textures[textureIndex] = texture.replace('_fancy', '')
|
||||||
|
pmpMap.header.data_size += len(texture)
|
||||||
|
hasChanged = True
|
||||||
|
if hasChanged:
|
||||||
|
self.logger.info(f"Patching {filePath}...")
|
||||||
|
f1.seek(0)
|
||||||
|
pmpMap.write_to_stream(f1)
|
||||||
|
else:
|
||||||
|
f1.close()
|
||||||
|
|
23
A25_A26/FixNewPlayerParent.py
Normal file
23
A25_A26/FixNewPlayerParent.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from utils.fixers.BaseFixer import BaseFixer
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import os
|
||||||
|
|
||||||
|
class FixNewPlayerParent(BaseFixer):
|
||||||
|
def __init__(self, vfs_root, verbose=False):
|
||||||
|
BaseFixer.__init__(self, vfs_root, verbose, __name__)
|
||||||
|
self.add_files(os.path.join('simulation', 'templates', 'special', 'players'), tuple(".xml"))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for file in self.files:
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
root.set('parent', "template_player")
|
||||||
|
self.save_xml_file(tree, root, file)
|
40
A25_A26/FormationFixer.py
Normal file
40
A25_A26/FormationFixer.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from utils.fixers.BaseFixer import BaseFixer
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import os
|
||||||
|
|
||||||
|
class FormationFixer(BaseFixer):
|
||||||
|
def __init__(self, vfs_root, verbose=False):
|
||||||
|
BaseFixer.__init__(self, vfs_root, verbose, __name__)
|
||||||
|
self.add_files(os.path.join('simulation', 'templates'), tuple(".xml"))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for file in self.files:
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
cmp_formation = root.find('Formation')
|
||||||
|
if cmp_formation is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cmp_identity = self.create_tag_if_not_exist(root, 'Identity')
|
||||||
|
formation_icon_tag = cmp_formation.find('Icon')
|
||||||
|
if formation_icon_tag is not None:
|
||||||
|
icon_tag = self.create_tag_if_not_exist(cmp_identity, 'Icon')
|
||||||
|
icon_tag.text = formation_icon_tag.text
|
||||||
|
|
||||||
|
cmp_formation.remove(formation_icon_tag)
|
||||||
|
|
||||||
|
formation_name_tag = cmp_formation.find('FormationName')
|
||||||
|
if formation_name_tag is not None:
|
||||||
|
generic_name_tag = self.create_tag_if_not_exist(cmp_identity, 'GenericName')
|
||||||
|
generic_name_tag.text = formation_name_tag.text
|
||||||
|
cmp_formation.remove(formation_name_tag)
|
||||||
|
|
||||||
|
self.save_xml_file(tree, root, file)
|
113
A25_A26/P256.py
Normal file
113
A25_A26/P256.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Freagarach
|
||||||
|
|
||||||
|
import fileinput
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
class SedLike:
|
||||||
|
def sed(path, changes):
|
||||||
|
for line in fileinput.input(path, inplace=True, encoding='utf-8'):
|
||||||
|
for change in changes:
|
||||||
|
line = line.replace(change[0], change[1])
|
||||||
|
print(line, end="")
|
||||||
|
|
||||||
|
class TemplateFixer:
|
||||||
|
def __init__(self, vfs_root):
|
||||||
|
self.template_folder = os.path.join(vfs_root, 'simulation', 'templates')
|
||||||
|
|
||||||
|
def fix_template(self, template_path):
|
||||||
|
tree = ET.parse(template_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
production_queue = root.find('ProductionQueue')
|
||||||
|
if production_queue == None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
technologies = production_queue.find('Technologies')
|
||||||
|
tech_cost_multiplier = production_queue.find('TechCostMultiplier')
|
||||||
|
if technologies != None or tech_cost_multiplier != None:
|
||||||
|
researcher = ET.Element('Researcher')
|
||||||
|
if (technologies != None):
|
||||||
|
researcher.append(technologies)
|
||||||
|
production_queue.remove(technologies)
|
||||||
|
if (tech_cost_multiplier != None):
|
||||||
|
researcher.append(tech_cost_multiplier)
|
||||||
|
production_queue.remove(tech_cost_multiplier)
|
||||||
|
researcher[:] = sorted(researcher, key=lambda x: x.tag)
|
||||||
|
root.append(researcher)
|
||||||
|
|
||||||
|
entities = production_queue.find('Entities')
|
||||||
|
batch_time_modifier = production_queue.find('BatchTimeModifier')
|
||||||
|
if entities != None or batch_time_modifier != None:
|
||||||
|
trainer = ET.Element('Trainer')
|
||||||
|
if (entities != None):
|
||||||
|
trainer.append(entities)
|
||||||
|
production_queue.remove(entities)
|
||||||
|
if (batch_time_modifier != None):
|
||||||
|
trainer.append(batch_time_modifier)
|
||||||
|
production_queue.remove(batch_time_modifier)
|
||||||
|
trainer[:] = sorted(trainer, key=lambda x: x.tag)
|
||||||
|
root.append(trainer)
|
||||||
|
|
||||||
|
if production_queue.get('disable') != None:
|
||||||
|
for element in ["Researcher", "Trainer"]:
|
||||||
|
existing_element = root.find(element)
|
||||||
|
if existing_element is None:
|
||||||
|
functionality = ET.Element(element)
|
||||||
|
functionality.set('disable', "")
|
||||||
|
root.append(functionality)
|
||||||
|
else:
|
||||||
|
existing_element.set('disable', "")
|
||||||
|
else:
|
||||||
|
root.remove(production_queue)
|
||||||
|
|
||||||
|
root[:] = sorted(root, key=lambda x: x.tag)
|
||||||
|
ET.indent(tree)
|
||||||
|
|
||||||
|
tree.write(template_path, xml_declaration=True, encoding='utf-8')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def fix_style(self, template_path):
|
||||||
|
self.changes = [
|
||||||
|
[' />', '/>'],
|
||||||
|
["version='1.0'", 'version="1.0"'],
|
||||||
|
["'utf-8'", '"utf-8"']
|
||||||
|
]
|
||||||
|
SedLike.sed(template_path, self.changes)
|
||||||
|
with open(template_path, 'a' , encoding='utf-8') as file:
|
||||||
|
file.write('\n')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for template in glob.iglob(self.template_folder + '/**/*.xml', recursive=True):
|
||||||
|
if self.fix_template(template):
|
||||||
|
print(template)
|
||||||
|
self.fix_style(template)
|
||||||
|
|
||||||
|
|
||||||
|
class ModifiersFixer:
|
||||||
|
def __init__(self, vfs_root):
|
||||||
|
self.data_folder = os.path.join(vfs_root, 'simulation', 'data')
|
||||||
|
self.changes = [
|
||||||
|
["ProductionQueue/Batch", "Trainer/Batch"],
|
||||||
|
["ProductionQueue/Ent", "Trainer/Ent"],
|
||||||
|
["ProductionQueue/Train", "Trainer/Train"],
|
||||||
|
["ProductionQueue/Tech", "Researcher/Tech"],
|
||||||
|
]
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for modification in glob.iglob(self.data_folder + '/**/*.json', recursive=True):
|
||||||
|
SedLike.sed(modification, self.changes)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
template_fixer = TemplateFixer(script_dir)
|
||||||
|
template_fixer.run()
|
||||||
|
|
||||||
|
modifier_fixer = ModifiersFixer(script_dir)
|
||||||
|
modifier_fixer.run()
|
52
A25_A26/P259.py
Normal file
52
A25_A26/P259.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Vladislav Belov
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
def convert_ao(path):
|
||||||
|
if 'ao' not in path.lower():
|
||||||
|
return
|
||||||
|
print(path)
|
||||||
|
|
||||||
|
image = Image.open(path)
|
||||||
|
image_out = Image.new('L', image.size)
|
||||||
|
for y in range(image.size[1]):
|
||||||
|
for x in range(image.size[0]):
|
||||||
|
if image.mode in ['1', 'L', 'P']:
|
||||||
|
ao = float(image.getpixel((x, y))) / 255.0
|
||||||
|
else:
|
||||||
|
ao = float(image.getpixel((x, y))[0]) / 255.0
|
||||||
|
ao = 0.3 + ao * 2.0 * 0.7 # Might be adjusted for a particular mod.
|
||||||
|
gray = int(ao * 255)
|
||||||
|
if gray < 0:
|
||||||
|
gray = 0
|
||||||
|
elif gray > 255:
|
||||||
|
gray = 255
|
||||||
|
image_out.putpixel((x, y), (gray))
|
||||||
|
image_out.save(path)
|
||||||
|
|
||||||
|
def convert_recursively(path):
|
||||||
|
for file_name in os.listdir(path):
|
||||||
|
file_path = os.path.join(path, file_name)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
name, ext = os.path.splitext(file_name)
|
||||||
|
if ext.lower() in ['.png']:
|
||||||
|
convert_ao(file_path)
|
||||||
|
elif os.path.isdir(file_path):
|
||||||
|
convert_recursively(file_path)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
paths = sys.argv[1:]
|
||||||
|
for path in paths:
|
||||||
|
if os.path.isfile(path):
|
||||||
|
convert_ao(path)
|
||||||
|
elif os.path.isdir(path):
|
||||||
|
convert_recursively(path)
|
||||||
|
|
70
A25_A26/P261.py
Normal file
70
A25_A26/P261.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Freagarach
|
||||||
|
|
||||||
|
import fileinput
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
class SedLike:
|
||||||
|
def sed(path, changes):
|
||||||
|
for line in fileinput.input(path, inplace=True, encoding='utf-8'):
|
||||||
|
for change in changes:
|
||||||
|
line = line.replace(change[0], change[1])
|
||||||
|
print(line, end="")
|
||||||
|
|
||||||
|
class TemplateFixer:
|
||||||
|
def __init__(self, vfs_root):
|
||||||
|
self.template_folder = os.path.join(vfs_root, 'simulation', 'templates')
|
||||||
|
|
||||||
|
def fix_template(self, template_path):
|
||||||
|
tree = ET.parse(template_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
cmp_identity = root.find('Identity')
|
||||||
|
if cmp_identity == None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
formations = cmp_identity.find('Formations')
|
||||||
|
if formations == None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cmp_unitai = root.find('UnitAI')
|
||||||
|
if cmp_unitai == None:
|
||||||
|
cmp_unitai = ET.Element('UnitAI')
|
||||||
|
root.append(cmp_unitai)
|
||||||
|
cmp_unitai.append(formations)
|
||||||
|
cmp_identity.remove(formations)
|
||||||
|
|
||||||
|
if cmp_identity.__len__() == 0:
|
||||||
|
root.remove(cmp_identity)
|
||||||
|
|
||||||
|
root[:] = sorted(root, key=lambda x: x.tag)
|
||||||
|
ET.indent(tree)
|
||||||
|
|
||||||
|
tree.write(template_path, xml_declaration=True, encoding='utf-8')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def fix_style(self, template_path):
|
||||||
|
self.changes = [
|
||||||
|
[' />', '/>'],
|
||||||
|
["version='1.0'", 'version="1.0"'],
|
||||||
|
["'utf-8'", '"utf-8"']
|
||||||
|
]
|
||||||
|
SedLike.sed(template_path, self.changes)
|
||||||
|
with open(template_path, 'a', encoding='utf-8') as file:
|
||||||
|
file.write('\n')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for template in glob.iglob(self.template_folder + '/**/*.xml', recursive=True):
|
||||||
|
if self.fix_template(template):
|
||||||
|
print(template)
|
||||||
|
self.fix_style(template)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
template_fixer = TemplateFixer(script_dir)
|
||||||
|
template_fixer.run()
|
91
A25_A26/PlayerXMLFixer.py
Normal file
91
A25_A26/PlayerXMLFixer.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from utils.fixers.BaseFixer import BaseFixer
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
class PlayerXMLFixer(BaseFixer):
|
||||||
|
|
||||||
|
def __init__(self, vfs_root, verbose=False, name=__name__):
|
||||||
|
BaseFixer.__init__(self, vfs_root, verbose, name)
|
||||||
|
self.add_files(os.path.join('simulation', 'templates', 'special', 'player'), tuple(".xml"))
|
||||||
|
|
||||||
|
def list_civs(self):
|
||||||
|
|
||||||
|
civs = []
|
||||||
|
for root, _, files in os.walk(str(self.vfs_root)):
|
||||||
|
for name in files:
|
||||||
|
file_path = os.path.join(root, name)
|
||||||
|
if os.path.isfile(file_path) and os.path.join('simulation', 'data', 'civs') in file_path and '.json' in name:
|
||||||
|
civs.append({"Path" : file_path , "Code" : name.split('.')[0]})
|
||||||
|
|
||||||
|
return civs
|
||||||
|
|
||||||
|
def create_player_file(self):
|
||||||
|
root = ET.Element("Entity ")
|
||||||
|
root.set('parent', "special/player")
|
||||||
|
return root
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for civ in self.list_civs():
|
||||||
|
tree = None
|
||||||
|
root = None
|
||||||
|
xml_file = None
|
||||||
|
for file in self.files:
|
||||||
|
if civ["Code"] in file:
|
||||||
|
xml_file = file
|
||||||
|
break
|
||||||
|
|
||||||
|
if xml_file is not None:
|
||||||
|
tree = ET.parse(xml_file)
|
||||||
|
root = tree.getroot()
|
||||||
|
root.set('parent', "special/player")
|
||||||
|
else:
|
||||||
|
root = self.create_player_file()
|
||||||
|
tree = ET.ElementTree(root)
|
||||||
|
|
||||||
|
|
||||||
|
data = None
|
||||||
|
with open(civ["Path"], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
cmp_identity = self.create_tag_if_not_exist(root, 'Identity')
|
||||||
|
|
||||||
|
if "History" in data.keys() and data["History"] is not None:
|
||||||
|
history_tag = self.create_tag_if_not_exist(cmp_identity, 'History')
|
||||||
|
history_tag.text = data["History"]
|
||||||
|
del data["History"]
|
||||||
|
|
||||||
|
if "Code" in data.keys():
|
||||||
|
civ_tag = self.create_tag_if_not_exist(cmp_identity, 'Civ')
|
||||||
|
civ_tag.text = data["Code"]
|
||||||
|
|
||||||
|
if "Name" in data.keys():
|
||||||
|
generic_name_tag = self.create_tag_if_not_exist(cmp_identity, 'GenericName')
|
||||||
|
generic_name_tag.text = data["Name"]
|
||||||
|
del data["Name"]
|
||||||
|
|
||||||
|
if "Emblem" in data.keys():
|
||||||
|
icon_tag = self.create_tag_if_not_exist(cmp_identity, 'Icon')
|
||||||
|
icon_tag.text = data["Emblem"]
|
||||||
|
del data["Emblem"]
|
||||||
|
|
||||||
|
outputFolder = Path(self.vfs_root) / 'simulation' / 'templates' / 'special' / 'players'
|
||||||
|
os.makedirs(outputFolder, exist_ok=True)
|
||||||
|
outputFile = outputFolder / (civ["Code"] + '.xml')
|
||||||
|
outputFile.touch()
|
||||||
|
self.save_xml_file(tree, root, outputFile)
|
||||||
|
with open(civ["Path"], 'w', encoding='utf-8') as outfile:
|
||||||
|
json.dump(data, outfile, indent='\t')
|
||||||
|
|
||||||
|
oldPlayerFolder = Path(self.vfs_root) / 'simulation' / 'templates' / 'special' / 'player'
|
||||||
|
if oldPlayerFolder.exists():
|
||||||
|
shutil.rmtree(oldPlayerFolder)
|
60
A25_A26/PlayerXMLFormationFixer.py
Normal file
60
A25_A26/PlayerXMLFormationFixer.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from .PlayerXMLFixer import PlayerXMLFixer
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
class PlayerXMLFormationFixer(PlayerXMLFixer):
|
||||||
|
|
||||||
|
def __init__(self, vfs_root, verbose=False):
|
||||||
|
super(PlayerXMLFixer, self).__init__(vfs_root, verbose, __name__)
|
||||||
|
self.add_files(os.path.join('simulation', 'templates', 'special', 'players'), tuple(".xml"))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for civ in self.list_civs():
|
||||||
|
tree = None
|
||||||
|
root = None
|
||||||
|
xml_file = None
|
||||||
|
for file in self.files:
|
||||||
|
if civ["Code"] in file:
|
||||||
|
xml_file = file
|
||||||
|
break
|
||||||
|
|
||||||
|
if xml_file is not None:
|
||||||
|
tree = ET.parse(xml_file)
|
||||||
|
root = tree.getroot()
|
||||||
|
root.set('parent', "special/player")
|
||||||
|
else:
|
||||||
|
root = self.create_player_file()
|
||||||
|
tree = ET.ElementTree(root)
|
||||||
|
|
||||||
|
|
||||||
|
data = None
|
||||||
|
with open(civ["Path"], 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cmp_player = self.create_tag_if_not_exist(root, 'Player')
|
||||||
|
if "Formations" in data.keys() and data["Formations"] is not None:
|
||||||
|
formations_tag = self.create_tag_if_not_exist(cmp_player, 'Formations')
|
||||||
|
formations_tag.text = " ".join(data["Formations"])
|
||||||
|
|
||||||
|
del data["Formations"]
|
||||||
|
|
||||||
|
|
||||||
|
outputFolder = Path(self.vfs_root) / 'simulation' / 'templates' / 'special' / 'players'
|
||||||
|
os.makedirs(outputFolder, exist_ok=True)
|
||||||
|
outputFile = outputFolder / (civ["Code"] + '.xml')
|
||||||
|
outputFile.touch()
|
||||||
|
self.save_xml_file(tree, root, outputFile)
|
||||||
|
with open(civ["Path"], 'w', encoding='utf-8') as outfile:
|
||||||
|
json.dump(data, outfile, indent='\t', ensure_ascii=False)
|
35
A25_A26/SessionIconPathFixer.py
Normal file
35
A25_A26/SessionIconPathFixer.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from utils.fixers.BaseFixer import BaseFixer
|
||||||
|
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
class SessionIconPathFixer(BaseFixer):
|
||||||
|
|
||||||
|
def __init__(self, vfs_root, verbose=False):
|
||||||
|
BaseFixer.__init__(self, vfs_root, verbose, __name__)
|
||||||
|
self.add_files(os.path.join('simulation', 'templates', 'special', 'players'), tuple(".xml"))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for file in self.files:
|
||||||
|
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
cmp_identity = root.find('Identity')
|
||||||
|
if cmp_identity is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
icon_tag = cmp_identity.find('Icon')
|
||||||
|
if icon_tag is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
icon_tag.text = str(icon_tag.text).replace("session/portraits/", "")
|
||||||
|
self.save_xml_file(tree, root, file)
|
||||||
|
self.fix_style(file)
|
95
utils/PMPMap.py
Normal file
95
utils/PMPMap.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from io import BufferedReader, BufferedWriter
|
||||||
|
|
||||||
|
class PmpHeader():
|
||||||
|
def __init__(self, stream : BufferedReader):
|
||||||
|
self.magic = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
self.version = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
self.data_size = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
self.map_size = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
stream.write(self.magic.to_bytes(4, 'little'))
|
||||||
|
stream.write(self.version.to_bytes(4, 'little'))
|
||||||
|
stream.write(self.data_size.to_bytes(4, 'little'))
|
||||||
|
stream.write(self.map_size.to_bytes(4, 'little'))
|
||||||
|
|
||||||
|
class PmpHeightMap(list):
|
||||||
|
def __init__(self, stream : BufferedReader, width : int, height : int):
|
||||||
|
self.capacity = (width * 16 + 1) * (height * 16 + 1)
|
||||||
|
for _ in range (0, self.capacity):
|
||||||
|
self.append(int.from_bytes(stream.read(2), byteorder='little'))
|
||||||
|
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
for height_data in self:
|
||||||
|
stream.write(height_data.to_bytes(2, byteorder='little'))
|
||||||
|
|
||||||
|
class PmpTextures(list):
|
||||||
|
def __init__(self, stream : BufferedReader):
|
||||||
|
self.capacity = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
for _ in range (0, self.capacity):
|
||||||
|
length = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
self.append(stream.read(length).decode())
|
||||||
|
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
stream.write((len(self)).to_bytes(4, byteorder='little'))
|
||||||
|
for texture in self:
|
||||||
|
stream.write(len(texture).to_bytes(4, byteorder='little'))
|
||||||
|
stream.write(texture.encode())
|
||||||
|
|
||||||
|
class PmpTiles(list):
|
||||||
|
def __init__(self, stream : BufferedReader, capacity : int):
|
||||||
|
for _ in range(0, capacity):
|
||||||
|
self.append(PmpTile(stream))
|
||||||
|
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
for tile in self:
|
||||||
|
tile.write_to_stream(stream)
|
||||||
|
|
||||||
|
class PmpTile():
|
||||||
|
def __init__(self, stream : BufferedReader):
|
||||||
|
self.texture1 = int.from_bytes(stream.read(2), byteorder='little')
|
||||||
|
self.texture2 = int.from_bytes(stream.read(2), byteorder='little')
|
||||||
|
self.priority = int.from_bytes(stream.read(4), byteorder='little')
|
||||||
|
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
stream.write(self.texture1.to_bytes(2, byteorder='little'))
|
||||||
|
stream.write(self.texture2.to_bytes(2, byteorder='little'))
|
||||||
|
stream.write(self.priority.to_bytes(4, byteorder='little'))
|
||||||
|
|
||||||
|
class PmpPatch():
|
||||||
|
def __init__(self, stream : BufferedReader):
|
||||||
|
self.TILE_SIZE = 16 * 16
|
||||||
|
self.tiles = PmpTiles(stream, self.TILE_SIZE)
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
self.tiles.write_to_stream(stream)
|
||||||
|
|
||||||
|
class PmpPatches(list):
|
||||||
|
def __init__(self, stream, width, height):
|
||||||
|
self.capacity = width * height
|
||||||
|
for _ in range (0, self.capacity):
|
||||||
|
self.append(PmpPatch(stream))
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
for patch in self:
|
||||||
|
patch.write_to_stream(stream)
|
||||||
|
|
||||||
|
class PmpMap():
|
||||||
|
def __init__(self, stream : BufferedReader):
|
||||||
|
self.header = PmpHeader(stream)
|
||||||
|
self.heightMap = PmpHeightMap(stream, self.header.map_size, self.header.map_size)
|
||||||
|
self.textures = PmpTextures(stream)
|
||||||
|
self.patches = PmpPatches(stream, self.header.map_size, self.header.map_size)
|
||||||
|
|
||||||
|
def write_to_stream(self, stream : BufferedWriter):
|
||||||
|
self.header.write_to_stream(stream)
|
||||||
|
self.heightMap.write_to_stream(stream)
|
||||||
|
self.textures.write_to_stream(stream)
|
||||||
|
self.patches.write_to_stream(stream)
|
||||||
|
|
||||||
|
|
97
utils/fixers/BaseFixer.py
Normal file
97
utils/fixers/BaseFixer.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from ..logger_utils.InterceptableLogger import InterceptableLogger
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import fileinput
|
||||||
|
import os
|
||||||
|
|
||||||
|
class BaseFixer():
|
||||||
|
def __init__(self, vfs_root, verbose=False, name=__name__):
|
||||||
|
self.vfs_root = Path(vfs_root)
|
||||||
|
self.verbose = verbose
|
||||||
|
self.files = []
|
||||||
|
self.logger = InterceptableLogger(name)
|
||||||
|
|
||||||
|
def fix_style(self, xml_path):
|
||||||
|
changes = [
|
||||||
|
[' />', '/>'],
|
||||||
|
["version='1.0'", 'version="1.0"'],
|
||||||
|
["'utf-8'", '"utf-8"']
|
||||||
|
]
|
||||||
|
for line in fileinput.input(xml_path, inplace=True):
|
||||||
|
for change in changes:
|
||||||
|
line = line.replace(change[0], change[1])
|
||||||
|
print(line, end="")
|
||||||
|
|
||||||
|
with open(xml_path, 'a', encoding='utf-8') as file:
|
||||||
|
file.write('\n')
|
||||||
|
|
||||||
|
def indent(self, elem, level=0, more_sibs=False):
|
||||||
|
i = "\n"
|
||||||
|
if level:
|
||||||
|
i += (level-1) * ' '
|
||||||
|
num_kids = len(elem)
|
||||||
|
if num_kids:
|
||||||
|
if not elem.text or not elem.text.strip():
|
||||||
|
elem.text = i + " "
|
||||||
|
if level:
|
||||||
|
elem.text += ' '
|
||||||
|
count = 0
|
||||||
|
for kid in elem:
|
||||||
|
self.indent(kid, level+1, count < num_kids - 1)
|
||||||
|
count += 1
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
if more_sibs:
|
||||||
|
elem.tail += ' '
|
||||||
|
else:
|
||||||
|
if level and (not elem.tail or not elem.tail.strip()):
|
||||||
|
elem.tail = i
|
||||||
|
if more_sibs:
|
||||||
|
elem.tail += ' '
|
||||||
|
def sort(self, root):
|
||||||
|
# sort the first layer
|
||||||
|
root[:] = sorted(root, key=lambda child: (child.tag,child.get('name')))
|
||||||
|
|
||||||
|
# sort the second layer
|
||||||
|
for c in root:
|
||||||
|
c[:] = sorted(c, key=lambda child: (child.tag,child.get('name')))
|
||||||
|
for cp in c:
|
||||||
|
cp[:] = sorted(cp, key=lambda child: (child.tag,child.get('name')))
|
||||||
|
for scp in cp:
|
||||||
|
scp[:] = sorted(scp, key=lambda child: (child.tag,child.get('name')))
|
||||||
|
|
||||||
|
def save_xml_file(self, tree, root, xml_file, sort=True):
|
||||||
|
if sort:
|
||||||
|
self.sort(root)
|
||||||
|
self.indent(root)
|
||||||
|
tree.write(xml_file, xml_declaration=True, encoding='utf-8')
|
||||||
|
self.fix_style(xml_file)
|
||||||
|
|
||||||
|
def create_tag_if_not_exist(self, parent, name):
|
||||||
|
tag = parent.find(name)
|
||||||
|
if tag is None:
|
||||||
|
tag = ET.SubElement(parent, name)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def add_files(self, path, extensions : tuple[str]):
|
||||||
|
self.files = []
|
||||||
|
if os.path.isfile(str(self.vfs_root)):
|
||||||
|
self.files.append(self.vfs_root)
|
||||||
|
elif os.path.isdir(str(self.vfs_root)):
|
||||||
|
for root, _, files in os.walk(str(self.vfs_root)):
|
||||||
|
for name in files:
|
||||||
|
file_path = os.path.join(root, name)
|
||||||
|
if os.path.isfile(file_path) and path in file_path and name.endswith(extensions):
|
||||||
|
self.files.append(file_path)
|
||||||
|
if self.verbose:
|
||||||
|
if len(self.files) > 0:
|
||||||
|
self.logger.info(f"Found {len(self.files)} file(s).")
|
||||||
|
else:
|
||||||
|
self.logger.info(f"No files were found.")
|
35
utils/logger_utils/InterceptableLogger.py
Normal file
35
utils/logger_utils/InterceptableLogger.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from .SingleLevelFilter import SingleLevelFilter
|
||||||
|
|
||||||
|
from logging import getLogger, StreamHandler, INFO, WARNING, Formatter
|
||||||
|
from sys import stdout, stderr
|
||||||
|
|
||||||
|
class InterceptableLogger():
|
||||||
|
def __init__(self, logger_name : str):
|
||||||
|
self.logger = getLogger(logger_name)
|
||||||
|
self.logger.setLevel(INFO)
|
||||||
|
ch = StreamHandler(stdout)
|
||||||
|
ch.setLevel(INFO)
|
||||||
|
ch.setFormatter(Formatter('%(levelname)s - %(message)s'))
|
||||||
|
f1 = SingleLevelFilter(INFO, False)
|
||||||
|
ch.addFilter(f1)
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
errorch = StreamHandler(stderr)
|
||||||
|
errorch.setLevel(WARNING)
|
||||||
|
errorch.setFormatter(Formatter('%(levelname)s - %(message)s'))
|
||||||
|
self.logger.addHandler(errorch)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
self.logger.info(message)
|
||||||
|
|
||||||
|
def warn(self, message):
|
||||||
|
self.logger.warn(message)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
self.logger.error(message)
|
19
utils/logger_utils/SingleLevelFilter.py
Normal file
19
utils/logger_utils/SingleLevelFilter.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
||||||
|
# SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini
|
||||||
|
|
||||||
|
from logging import Filter
|
||||||
|
|
||||||
|
class SingleLevelFilter(Filter):
|
||||||
|
def __init__(self, passlevel, reject):
|
||||||
|
self.passlevel = passlevel
|
||||||
|
self.reject = reject
|
||||||
|
|
||||||
|
def filter(self, record):
|
||||||
|
if self.reject:
|
||||||
|
return (record.levelno != self.passlevel)
|
||||||
|
else:
|
||||||
|
return (record.levelno == self.passlevel)
|
Loading…
Reference in New Issue
Block a user