879862c9c3
- Carte du monde (WorldMap.lua) : fenetre reduite camelot 702 x 534 posee sur l'ossature de WotLK, fil d'Ariane, filtres, etages, coordonnees, deplacable par la barre du titre. - Journal de quetes (QuestLog.lua) : volet liste de camelot a droite de la carte (recherche, compteur, reperes, suivi, menus), L ouvre carte + volet ; page d'une quete (texte, objectifs, recompenses, Abandon / Share / Track) avec bouton Retour qui rend la carte d'avant. - Suivi de quetes (ObjectiveTracker.lua) : modules camelot Quests et Achievements a la place des gestionnaires d'affichage de WotLK, API des gestionnaires conservee ; tri et filtres de WotLK dans le bouton filtre ; deplacable par /fui, position par defaut de camelot. - Menus : les menus contextuels et sous-menus prennent la largeur de leur contenu, mesuree dans la police affichee. - Art : feuilles questmaplogatlas, questtracker, cadres et fonds ; outils tranches_atlas.py ; banc d'essai etendu (WatchFrame, journal, menus). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
292 lines
11 KiB
Python
292 lines
11 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/>.
|
|
|
|
"""Faire entrer une feuille d'atlas dans l'atelier, avec son apercu et sa table.
|
|
|
|
POURQUOI PAR FEUILLE ET NON PAR ELEMENT. Une feuille versee apporte tous ses
|
|
elements d'un coup : le prochain cadre qui en a besoin n'exige plus de toucher
|
|
a l'archive. Et le client ne lie qu'une texture par cadre, ce qui est tout
|
|
l'interet des atlas.
|
|
|
|
CE QU'IL FAIT. Pour chaque ligne de tools/feuilles_complements.txt :
|
|
1. exporte le .blp depuis le client moderne, par le pont de wow.export ;
|
|
2. exporte le meme fichier en .png, l'apercu qui se range a cote ;
|
|
3. les depose tous les deux dans data/art/interface/ForeverUI/... ;
|
|
puis regenere data/addon/ForeverUI/UIAtlas_06_complements.lua a partir de
|
|
l'index des atlas.
|
|
|
|
IL N'ECRIT PAS DANS LE CLIENT. L'atelier d'abord ; tools/deployer.py ensuite.
|
|
|
|
wow.export doit tourner avec sa source chargee (pont sur le port 9455).
|
|
"""
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
PONT = "http://127.0.0.1:9455"
|
|
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
OUTILS = os.path.join(RACINE, "tools")
|
|
ART = os.path.join(RACINE, "data", "art")
|
|
ADDON = os.path.join(RACINE, "data", "addon", "ForeverUI")
|
|
LISTE = os.path.join(OUTILS, "feuilles_complements.txt")
|
|
LISTE_SIMPLE = os.path.join(OUTILS, "fichiers_simples.txt")
|
|
INDEX = os.path.join(OUTILS, "atlas_dump.json")
|
|
PREFIXE = "ForeverUI"
|
|
BS = chr(92)
|
|
|
|
# Le temporaire de l'export, hors de l'atelier.
|
|
STAGING = os.path.join(os.environ.get("TEMP", "."), "foreverui_export")
|
|
|
|
_VARIANTE = re.compile("^(2x|4x|c[0-9]+)$")
|
|
|
|
|
|
def _demander(chemin, params):
|
|
url = PONT + chemin + "?" + urllib.parse.urlencode(params, doseq=True)
|
|
with urllib.request.urlopen(url, timeout=900) as r:
|
|
return json.loads(r.read().decode("utf-8"))
|
|
|
|
|
|
def _logique(nom):
|
|
"""Le nom sans ses suffixes de variante, tel que le code de camelot l'ecrit."""
|
|
parts = nom.lower().split("-")
|
|
while len(parts) > 1 and _VARIANTE.match(parts[-1]):
|
|
parts.pop()
|
|
return "-".join(parts)
|
|
|
|
|
|
# QUAND LE CLIENT NE NOMME PAS SON FICHIER.
|
|
#
|
|
# Le listfile est communautaire : il ne donne un nom qu'aux fichiers que
|
|
# quelqu'un a identifies. Les autres n'existent que par leur FileDataID, et
|
|
# wow.export les appelle "unknown_<id>". Le code de camelot, lui, connait le
|
|
# vrai nom -- il l'ecrit en clair -- mais rien ne permet de l'exporter par ce
|
|
# nom-la.
|
|
#
|
|
# Une ligne peut donc s'ecrire "source => nom voulu dans l'atelier" : on
|
|
# exporte par l'identifiant, et on range sous le nom de la source. L'atelier
|
|
# reste lisible, et l'identifiant demeure dans la liste pour retrouver le
|
|
# fichier plus tard.
|
|
_ALIAS = {}
|
|
|
|
|
|
def _lire(liste):
|
|
voulues = []
|
|
for ligne in io.open(liste, encoding="utf-8"):
|
|
ligne = ligne.split("#")[0].strip()
|
|
if not ligne:
|
|
continue
|
|
if "=>" in ligne:
|
|
source, voulu = [part.strip().lower() for part in ligne.split("=>", 1)]
|
|
_ALIAS[source] = voulu
|
|
voulues.append(source)
|
|
else:
|
|
voulues.append(ligne.lower())
|
|
return voulues
|
|
|
|
|
|
def feuilles_voulues():
|
|
return _lire(LISTE)
|
|
|
|
|
|
def fichiers_simples():
|
|
"""Des images a part entiere : elles entrent, mais ne sont pas decoupees."""
|
|
if not os.path.exists(LISTE_SIMPLE):
|
|
return []
|
|
return _lire(LISTE_SIMPLE)
|
|
|
|
|
|
def chemin_atelier(feuille, extension=".blp"):
|
|
"""interface/hud/x.blp -> data/art/interface/ForeverUI/hud/x.blp
|
|
|
|
Un fichier sans nom dans le listfile se range sous celui que la source
|
|
lui donne, pas sous son "unknown_<id>".
|
|
"""
|
|
feuille = _ALIAS.get(feuille, feuille)
|
|
morceaux = feuille.split("/")
|
|
assert morceaux[0] == "interface", feuille
|
|
return os.path.join(ART, "interface", PREFIXE, *morceaux[1:])[:-4] + extension
|
|
|
|
|
|
def exporter(voulues):
|
|
manquantes = [f for f in voulues if not os.path.exists(chemin_atelier(f))]
|
|
sans_apercu = [f for f in voulues
|
|
if os.path.exists(chemin_atelier(f))
|
|
and not os.path.exists(chemin_atelier(f, ".png"))]
|
|
if not manquantes and not sans_apercu:
|
|
print("art : les %d feuilles sont deja dans l'atelier, avec leur apercu" % len(voulues))
|
|
return 0
|
|
|
|
statut = _demander("/status", {})
|
|
if not statut["ready"]:
|
|
_demander("/load-source", {})
|
|
raise SystemExit("la source de wow.export se charge, relancer dans une minute")
|
|
|
|
os.makedirs(STAGING, exist_ok=True)
|
|
poses = 0
|
|
|
|
if manquantes:
|
|
params = [("dest", STAGING)]
|
|
for f in manquantes:
|
|
if f.startswith("fdid:"):
|
|
params.append(("fdid", f[5:].split(".")[0]))
|
|
else:
|
|
params.append(("file", f))
|
|
res = _demander("/export", params)
|
|
if res["failed"]:
|
|
raise SystemExit("export en echec : %s" % res)
|
|
for f in manquantes:
|
|
cible = chemin_atelier(f)
|
|
os.makedirs(os.path.dirname(cible), exist_ok=True)
|
|
if f.startswith("fdid:"):
|
|
# exporte par identifiant : le pont rend le chemin exact
|
|
trouve = None
|
|
for e in res["exported"]:
|
|
if str(e.get("fileDataID")) == f[5:].split(".")[0]:
|
|
trouve = e["path"]
|
|
break
|
|
if not trouve:
|
|
raise SystemExit("export par identifiant sans chemin : %s" % f)
|
|
shutil.copyfile(trouve, cible)
|
|
else:
|
|
shutil.copyfile(os.path.join(STAGING, f.replace("/", os.sep)), cible)
|
|
print(" %-60s %8d o" % (os.path.relpath(cible, RACINE), os.path.getsize(cible)))
|
|
poses += 1
|
|
|
|
for f in manquantes + sans_apercu:
|
|
# mask 15 : garder la couche alpha. La configuration de l'utilisateur
|
|
# est a 7, ce qui donnerait un apercu opaque, inutilisable pour juger
|
|
# d'un art d'interface.
|
|
# L'APERCU passe par l'exporteur du logiciel, dont le parametre est
|
|
# toujours "file" : un identifiant s'y donne tel quel.
|
|
cle = ("file", f[5:].split(".")[0]) if f.startswith("fdid:") else ("file", f)
|
|
res = _demander("/export-textures", [("dest", STAGING), ("format", "png"),
|
|
("mask", "15"), cle])
|
|
if res["failed"]:
|
|
print(" PAS D'APERCU pour %s : %s" % (f, res["exported"]))
|
|
continue
|
|
source = res["exported"][0]["path"]
|
|
cible = chemin_atelier(f, ".png")
|
|
os.makedirs(os.path.dirname(cible), exist_ok=True)
|
|
shutil.copyfile(source, cible)
|
|
|
|
return poses
|
|
|
|
|
|
def regenerer_table(voulues):
|
|
"""La table d'atlas, regeneree pour les feuilles voulues.
|
|
|
|
UNE FEUILLE QUE LE LISTFILE NE NOMME PAS n'a pas de `file` dans l'index :
|
|
ses elements n'y sont ranges que par FileDataID. On la reconnait alors a
|
|
l'identifiant qui figure dans son alias -- "fdid:8198433 => interface/..."
|
|
-- et on lui donne le chemin d'atelier qu'on a choisi.
|
|
"""
|
|
index = json.load(io.open(INDEX, encoding="utf-8"))["results"]
|
|
|
|
# Une feuille demandee sous un alias se reconnait a son nom d'ATELIER,
|
|
# celui sous lequel on l'a rangee, et non a sa source.
|
|
voulues = [_ALIAS.get(f, f) for f in voulues]
|
|
|
|
# Les feuilles demandees par identifiant : fdid -> chemin voulu.
|
|
par_identifiant = {}
|
|
for source, voulu in _ALIAS.items():
|
|
if source.startswith("fdid:"):
|
|
par_identifiant[int(source[5:].split(".")[0])] = voulu.lower()
|
|
|
|
par_feuille = {}
|
|
for r in index:
|
|
if not r["atlas"]:
|
|
continue
|
|
# Un alias par identifiant l'emporte sur le chemin de l'index : le
|
|
# listfile en donne parfois un faux (uiframe.blp y est rangee sous
|
|
# interface/interface/framegeneral/).
|
|
nom = par_identifiant.get(r.get("fileDataID")) or (r.get("file") or "").lower()
|
|
if nom in voulues:
|
|
par_feuille.setdefault(nom, []).append(r)
|
|
|
|
absentes = [f for f in voulues if f not in par_feuille]
|
|
if absentes:
|
|
raise SystemExit("feuilles absentes de l'index des atlas : %s" % absentes)
|
|
|
|
lignes = [
|
|
"-- complements : feuilles ajoutees hors des cinq ecrans inventories.",
|
|
"-- genere par tools/ajouter_feuilles.py depuis tools/atlas_dump.json",
|
|
"",
|
|
"UIAtlas = UIAtlas or { sheets = {}, data = {} }",
|
|
"",
|
|
"local S = {",
|
|
]
|
|
ordre = sorted(par_feuille)
|
|
numero = {}
|
|
for i, f in enumerate(ordre, 1):
|
|
numero[f] = i
|
|
t = par_feuille[f][0]["sheet"]
|
|
chemin = ("interface/" + PREFIXE + "/" + f[len("interface/"):])[:-4].replace("/", BS + BS)
|
|
lignes.append('\t[%d] = "%s", -- %d x %d' % (i, chemin, t["width"], t["height"]))
|
|
lignes += ["}", "", "local D = {"]
|
|
|
|
total = 0
|
|
deja = set()
|
|
for f in ordre:
|
|
# Une image en double densite s'affiche a la moitie de sa taille.
|
|
facteur = 0.5 if "2x" in os.path.basename(f) else 1
|
|
for r in sorted(par_feuille[f], key=lambda x: x["atlas"].lower()):
|
|
sw, sh = r["sheet"]["width"], r["sheet"]["height"]
|
|
reg = r["region"]
|
|
u1 = reg["left"] / float(sw)
|
|
u2 = (reg["left"] + reg["width"]) / float(sw)
|
|
v1 = reg["top"] / float(sh)
|
|
v2 = (reg["top"] + reg["height"]) / float(sh)
|
|
largeur = int(round(reg["width"] * facteur))
|
|
hauteur = int(round(reg["height"] * facteur))
|
|
|
|
# Le nom brut, et le nom logique que le code de camelot emploie.
|
|
for nom in (r["atlas"].lower(), _logique(r["atlas"])):
|
|
if nom in deja:
|
|
continue
|
|
deja.add(nom)
|
|
lignes.append('\t["%s"] = {%d, %.6f, %.6f, %.6f, %.6f, %d, %d},' % (
|
|
nom, numero[f], u1, u2, v1, v2, largeur, hauteur))
|
|
total += 1
|
|
|
|
lignes += ["}", "",
|
|
"for name, entry in pairs(D) do",
|
|
"\tentry[1] = S[entry[1]]",
|
|
"\tUIAtlas.data[name] = entry",
|
|
"end",
|
|
""]
|
|
|
|
dest = os.path.join(ADDON, "UIAtlas_06_complements.lua")
|
|
io.open(dest, "w", encoding="utf-8", newline="\n").write("\n".join(lignes))
|
|
print("table des complements : %d feuilles, %d noms, %d octets" % (
|
|
len(ordre), total, os.path.getsize(dest)))
|
|
|
|
|
|
def main():
|
|
voulues = feuilles_voulues()
|
|
simples = fichiers_simples()
|
|
exporter(voulues + simples)
|
|
regenerer_table(voulues)
|
|
print("l'atelier est a jour ; poser dans le client avec tools/deployer.py")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|