Files
moon-well-ui/tools/deployer.py
T
Adrestie 1de02d13a0 Ecrans d'accueil a la DA de camelot : connexion, menu, selection, dialogues et creation de personnage
- GlueXML ForeverUI (data/glue) : cadre 2:1, polices, atlas, gabarits ;
  connexion, menu, selection des personnages, dialogues, creation en deux
  etapes (races et classes ; personnalisation numerotee, listes ouvrantes,
  echantillons de couleur, apercu differe, nom en bas de l'ecran)
- choix valides calcules sur CharSections aux regles du moteur
  (tools/choix_personnalisation.py) ; couleurs des echantillons lues dans
  les textures du client (tools/couleurs_personnalisation.py), peau teintee
  par la lumiere du decor
- decors de la creation et de la selection reduits a 0,912 pour compenser
  le cadre 2:1 (tools/decors_accueil.py) ; deployer.py suit aussi
  Interface\Glues\Models
- patcheur optionnel de Wow.exe (tools/patcheur, source et executable) :
  CycleCharCustomization rend l'indice du reglage
- art des ecrans d'accueil (data/art), outils de decoupe, d'atlas et de
  donnees
- barre du bas : boutons JcJ et Aide retires, bandeau a la largeur de ses
  boutons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:44:01 +02:00

293 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# This file is part of mod-forever-ui.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Poser ForeverUI dans un client 3.3.5 : l'addon d'un cote, l'art et les ecrans d'accueil dans patch-Z.
POURQUOI CE SCRIPT EXISTE. Le depot est la source, le client n'en est qu'une
copie. Editer dans Interface\\AddOns revient a travailler dans la copie : le
jour ou on verse depuis le depot, le travail disparait. On developpe donc ici,
et on pose la avec ce script.
CE QU'IL FAIT.
addon : recopie data/addon/ForeverUI dans le client, et retire du client les
fichiers qui ne sont plus dans le depot.
art : verse data/art/**.blp dans patch-Z.MPQ sous leur propre chemin, et
les decors des ecrans d'accueil (data/art/Interface/Glues/Models,
tools/decors_accueil.py) a la place de ceux du client. Les
.png voisins sont des apercus pour l'oeil humain : ils ne partent
pas dans l'archive. Un fichier deja identique n'est pas reverse, et
un fichier de ForeverUI que le depot ne porte plus est RETIRE de
l'archive.
glue : verse data/glue/Interface/GlueXML/** dans patch-Z sous
Interface\\GlueXML : les ecrans d'accueil, ou aucun addon ne tourne.
Meme regle que l'art : sous ce chemin, l'archive porte exactement le
depot, rien de plus.
LA REGLE. Ce qui est sous Interface\\ForeverUI et sous Interface\\Glues\\Models
dans le patch doit etre le calque exact de data/art : ni fichier en plus, ni
fichier different. Editer
directement dans le client ou laisser trainer une feuille versee autrefois
casse cette egalite, et plus personne ne sait ce que le jeu affiche.
L'archive est verrouillee tant que le client tourne : fermer le jeu avant.
"""
import argparse
import hashlib
import io
import os
import sys
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(RACINE, "tools"))
from foreverui import mpq # noqa: E402
CLIENT_DEFAUT = r"E:\world of warcraft 3.3.5a hd"
NOM_ADDON = "ForeverUI"
PREFIXE_ARCHIVE = "interface" + os.sep + "ForeverUI" + os.sep
PREFIXE_GLUE = "Interface" + os.sep + "GlueXML" + os.sep
PREFIXE_DECORS = "Interface" + os.sep + "Glues" + os.sep + "Models" + os.sep
# Les fichiers de l'addon que les ecrans d'accueil emploient aussi : une seule
# source, versee sous un nom a elle dans Interface\GlueXML (fichier de
# l'addon -> nom dans l'archive).
PARTAGES_GLUE = {
"ScrollBar.lua": "ForeverUIScrollBar.lua",
}
def _empreinte(donnees):
return hashlib.sha1(donnees).hexdigest()
def poser_addon(client, bavard=True):
source = os.path.join(RACINE, "data", "addon", NOM_ADDON)
cible = os.path.join(client, "Interface", "AddOns", NOM_ADDON)
os.makedirs(cible, exist_ok=True)
attendus = set()
poses, inchanges = 0, 0
for nom in sorted(os.listdir(source)):
if not (nom.endswith(".lua") or nom.endswith(".toc")):
continue
attendus.add(nom)
brut = io.open(os.path.join(source, nom), "rb").read()
chemin = os.path.join(cible, nom)
if os.path.exists(chemin) and io.open(chemin, "rb").read() == brut:
inchanges += 1
continue
io.open(chemin, "wb").write(brut)
poses += 1
retires = []
for nom in sorted(os.listdir(cible)):
if (nom.endswith(".lua") or nom.endswith(".toc")) and nom not in attendus:
os.remove(os.path.join(cible, nom))
retires.append(nom)
if bavard:
print("addon : %d pose(s), %d inchange(s), %d retire(s) %s" % (
poses, inchanges, len(retires), retires or ""))
return poses, retires
def _art_de_l_archive(archive, prefixe=PREFIXE_ARCHIVE):
"""Ce que l'archive porte sous un prefixe (Interface\\ForeverUI par
defaut), d'apres son listfile."""
if not archive.has("(listfile)"):
return set()
texte = archive.read("(listfile)").decode("utf-8", "replace")
porte = set()
for ligne in texte.splitlines():
nom = ligne.strip().replace("/", os.sep)
if nom.lower().startswith(prefixe.lower()):
porte.add(nom)
return porte
def _fichiers_art(prefixe=PREFIXE_ARCHIVE):
"""Chemin interne dans l'archive -> chemin sur le disque, sous prefixe."""
base = os.path.join(RACINE, "data", "art")
trouves = {}
for racine, _dossiers, fichiers in os.walk(base):
for nom in fichiers:
# l'art, et les decors des ecrans d'accueil (tools/decors_accueil.py)
if not nom.lower().endswith((".blp", ".m2", ".skin")):
continue
chemin = os.path.join(racine, nom)
interne = os.path.relpath(chemin, base).replace("/", os.sep)
if interne.lower().startswith(prefixe.lower()):
trouves[interne] = chemin
return trouves
def _fichiers_glue():
"""Chemin interne dans l'archive -> chemin sur le disque."""
base = os.path.join(RACINE, "data", "glue")
trouves = {}
# sans ecrans d'accueil dans le depot, rien -- pas meme les fichiers
# partages : l'archive redevient celle du client
if not os.path.isdir(base):
return trouves
for racine, _dossiers, fichiers in os.walk(base):
for nom in fichiers:
if not nom.lower().endswith((".lua", ".xml", ".toc")):
continue
chemin = os.path.join(racine, nom)
interne = os.path.relpath(chemin, base).replace("/", os.sep)
assert interne.lower().startswith(PREFIXE_GLUE.lower()), interne
trouves[interne] = chemin
for source, nom in PARTAGES_GLUE.items():
trouves[PREFIXE_GLUE + nom] = os.path.join(RACINE, "data", "addon", NOM_ADDON, source)
return trouves
def _archive_z(client):
archive_chemin = os.path.join(client, "data", "patch-Z.MPQ")
if not os.path.exists(archive_chemin):
archive_chemin = os.path.join(client, "Data", "patch-Z.MPQ")
if not os.path.exists(archive_chemin):
raise SystemExit("patch-Z.MPQ introuvable dans %s" % client)
return archive_chemin
def poser_art(client, bavard=True):
return (_verser(client, _fichiers_art(), PREFIXE_ARCHIVE, "art", bavard)
+ _verser(client, _fichiers_art(PREFIXE_DECORS), PREFIXE_DECORS, "decors", bavard))
def poser_glue(client, bavard=True):
return _verser(client, _fichiers_glue(), PREFIXE_GLUE, "glue", bavard)
def _verser(client, art, prefixe, titre, bavard=True):
archive_chemin = _archive_z(client)
# Ne reverser que ce qui a change, et relever ce qui n'a plus lieu d'etre.
a_verser = {}
a_retirer = []
archive = mpq.Archive(archive_chemin)
try:
for interne, chemin in sorted(art.items()):
brut = io.open(chemin, "rb").read()
if archive.has(interne) and _empreinte(archive.read(interne)) == _empreinte(brut):
continue
a_verser[interne] = brut
attendus = set(n.lower() for n in art)
a_retirer = sorted(n for n in _art_de_l_archive(archive, prefixe)
if n.lower() not in attendus)
finally:
archive.close()
if not a_verser and not a_retirer:
if bavard:
print("%s : %d fichiers, l'archive est deja le calque du depot" % (titre, len(art)))
return 0
avant = os.path.getsize(archive_chemin)
try:
mpq.patch_archive(archive_chemin, a_verser, remove=a_retirer)
except PermissionError:
raise SystemExit("patch-Z.MPQ est verrouille : fermer le client avant de verser.")
archive = mpq.Archive(archive_chemin)
try:
anomalies = [n for n, brut in a_verser.items()
if _empreinte(archive.read(n)) != _empreinte(brut)]
finally:
archive.close()
if bavard:
print("%s : %d verse(s) sur %d, %d retire(s) | archive %.3f -> %.3f Gio | relecture : %d anomalie(s)" % (
titre, len(a_verser), len(art), len(a_retirer), avant / (1024.0 ** 3),
os.path.getsize(archive_chemin) / (1024.0 ** 3), len(anomalies)))
for n in a_retirer:
print(" retire : %s" % n)
if anomalies:
raise SystemExit("relecture fausse : %s" % ", ".join(anomalies))
return len(a_verser) + len(a_retirer)
def verifier(client):
"""Ce qui est pose correspond-il encore au depot ?"""
source = os.path.join(RACINE, "data", "addon", NOM_ADDON)
cible = os.path.join(client, "Interface", "AddOns", NOM_ADDON)
ecarts = []
for nom in sorted(os.listdir(source)):
if not (nom.endswith(".lua") or nom.endswith(".toc")):
continue
pose = os.path.join(cible, nom)
if not os.path.exists(pose):
ecarts.append("absent du client : %s" % nom)
elif io.open(pose, "rb").read() != io.open(os.path.join(source, nom), "rb").read():
ecarts.append("different : %s" % nom)
archive_chemin = os.path.join(client, "data", "patch-Z.MPQ")
if os.path.exists(archive_chemin):
archive = mpq.Archive(archive_chemin)
try:
for art, prefixe in ((_fichiers_art(), PREFIXE_ARCHIVE), (_fichiers_glue(), PREFIXE_GLUE),
(_fichiers_art(PREFIXE_DECORS), PREFIXE_DECORS)):
for interne, chemin in sorted(art.items()):
brut = io.open(chemin, "rb").read()
if not archive.has(interne):
ecarts.append("absent de l'archive : %s" % interne)
elif _empreinte(archive.read(interne)) != _empreinte(brut):
ecarts.append("different dans l'archive : %s" % interne)
attendus = set(n.lower() for n in art)
for n in sorted(_art_de_l_archive(archive, prefixe)):
if n.lower() not in attendus:
ecarts.append("en trop dans l'archive : %s" % n)
finally:
archive.close()
if ecarts:
print("ecarts entre le depot et le client : %d" % len(ecarts))
for e in ecarts:
print(" %s" % e)
else:
print("le client est conforme au depot")
return ecarts
def main():
analyseur = argparse.ArgumentParser(description=__doc__.splitlines()[0])
analyseur.add_argument("--client", default=CLIENT_DEFAUT, help="dossier du client 3.3.5")
analyseur.add_argument("--addon", action="store_true", help="ne poser que l'addon")
analyseur.add_argument("--art", action="store_true", help="ne verser que l'art")
analyseur.add_argument("--glue", action="store_true", help="ne verser que les ecrans d'accueil")
analyseur.add_argument("--verifier", action="store_true",
help="ne rien ecrire, seulement comparer")
options = analyseur.parse_args()
if options.verifier:
verifier(options.client)
return
tout = not (options.addon or options.art or options.glue)
if tout or options.addon:
poser_addon(options.client)
if tout or options.art:
poser_art(options.client)
if tout or options.glue:
poser_glue(options.client)
if __name__ == "__main__":
main()