plotagon-director 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plotagon_director/__init__.py +3 -0
- plotagon_director/audio/__init__.py +0 -0
- plotagon_director/audio/phonemes.py +70 -0
- plotagon_director/audio/tts.py +163 -0
- plotagon_director/cli.py +308 -0
- plotagon_director/core/__init__.py +0 -0
- plotagon_director/core/archive.py +35 -0
- plotagon_director/core/decompiler.py +120 -0
- plotagon_director/core/diffplot.py +89 -0
- plotagon_director/core/doctor.py +67 -0
- plotagon_director/core/plotdoc.py +178 -0
- plotagon_director/core/validator.py +165 -0
- plotagon_director/library/__init__.py +14 -0
- plotagon_director/library/builder.py +153 -0
- plotagon_director/library/data/library.json +4819 -0
- plotagon_director-0.6.0.dist-info/METADATA +143 -0
- plotagon_director-0.6.0.dist-info/RECORD +21 -0
- plotagon_director-0.6.0.dist-info/WHEEL +5 -0
- plotagon_director-0.6.0.dist-info/entry_points.txt +2 -0
- plotagon_director-0.6.0.dist-info/licenses/LICENSE +21 -0
- plotagon_director-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Construit library/data/library.json depuis le manifeste officiel local.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m plotagon_director.library.builder [chemin/vers/builtincontentmanifest.json]
|
|
5
|
+
|
|
6
|
+
Par defaut, cherche le manifeste dans une installation Plotagon Studio situee
|
|
7
|
+
dans le dossier parent du depot (ex. Desktop/plotagon/Plotagon Studio_Data).
|
|
8
|
+
|
|
9
|
+
En plus du manifeste, extrait les positions d'acteurs de chaque scene depuis
|
|
10
|
+
les bundles Content/scenes/*.pcf : chaque .pcf embarque un JSON listant les
|
|
11
|
+
slots ("locationID", "displayName", "stance"...) — confirme par un export
|
|
12
|
+
officiel a 2 personnages (positions "infodesk2"/"middle1" sur stores.tech).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from . import DATA_PATH
|
|
21
|
+
|
|
22
|
+
DEFAULT_MANIFEST = (
|
|
23
|
+
Path(__file__).resolve().parents[3]
|
|
24
|
+
/ "Plotagon Studio_Data" / "StreamingAssets" / "JSON"
|
|
25
|
+
/ "ContentManifest" / "builtincontentmanifest.json"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
EXPRESSION_PREFIX = "animation.dialogue."
|
|
29
|
+
|
|
30
|
+
_LOCATION_PATTERN = re.compile(
|
|
31
|
+
rb'"locationID":\s*"([^"]+)",\s*"name":\s*"([^"]*)",\s*"displayName":\s*"([^"]*)"'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_STRING_PATTERN = re.compile(rb"[ -~]{3,}")
|
|
35
|
+
_ID_PATTERN = re.compile(r"[a-z][a-z0-9_]{2,}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_actions(animation_db_path, expressions):
|
|
39
|
+
"""Extrait les actions (interactions) de AnimationMetaDatabase.animationdata.
|
|
40
|
+
|
|
41
|
+
La base (bundle UnityFS, lu via UnityPy) contient des enregistrements
|
|
42
|
+
[id, Nom, phrase verbale, reaction, stance] pour les animations de
|
|
43
|
+
dialogue ET les interactions. Les interactions = ids absents de la liste
|
|
44
|
+
des expressions du manifeste. Confirme par l'export officiel
|
|
45
|
+
'forcechoke' / "force chokes". Les ids idle* (internes) sont exclus.
|
|
46
|
+
|
|
47
|
+
Retourne {id: {"text": Nom, "phrase": phrase}} ou {} si UnityPy absent.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
import UnityPy
|
|
51
|
+
except ImportError:
|
|
52
|
+
print("UnityPy absent: actions non extraites (pip install UnityPy)")
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
env = UnityPy.load(str(animation_db_path))
|
|
56
|
+
raw = next(
|
|
57
|
+
o for o in env.objects if o.type.name == "MonoBehaviour"
|
|
58
|
+
).get_raw_data()
|
|
59
|
+
strings = [m.group(0).decode() for m in _STRING_PATTERN.finditer(raw)]
|
|
60
|
+
|
|
61
|
+
records = {}
|
|
62
|
+
for i, s in enumerate(strings[:-2]):
|
|
63
|
+
if not _ID_PATTERN.fullmatch(s):
|
|
64
|
+
continue
|
|
65
|
+
display = strings[i + 1]
|
|
66
|
+
if display.lower().replace(" ", "").replace("_", "") != s.replace("_", ""):
|
|
67
|
+
continue
|
|
68
|
+
records.setdefault(s, {"text": display, "phrase": strings[i + 2]})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
k: v for k, v in sorted(records.items())
|
|
72
|
+
if k not in expressions and not k.startswith("idle")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_scene_positions(scenes_dir):
|
|
77
|
+
"""Scanne Content/scenes/*.pcf et retourne {scene_id: [{id, text}, ...]}."""
|
|
78
|
+
positions = {}
|
|
79
|
+
for pcf in sorted(Path(scenes_dir).glob("*.pcf")):
|
|
80
|
+
# nom de fichier: <scene.id>.<hash>.pcf
|
|
81
|
+
scene_id = pcf.name.rsplit(".", 2)[0]
|
|
82
|
+
data = pcf.read_bytes()
|
|
83
|
+
slots = []
|
|
84
|
+
seen = set()
|
|
85
|
+
for m in _LOCATION_PATTERN.finditer(data):
|
|
86
|
+
loc_id = m.group(1).decode("utf-8", "replace")
|
|
87
|
+
if loc_id in seen or loc_id == "None":
|
|
88
|
+
continue
|
|
89
|
+
seen.add(loc_id)
|
|
90
|
+
slots.append({
|
|
91
|
+
"id": loc_id,
|
|
92
|
+
"text": m.group(3).decode("utf-8", "replace"),
|
|
93
|
+
})
|
|
94
|
+
if slots:
|
|
95
|
+
positions[scene_id] = slots
|
|
96
|
+
return positions
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_library(manifest_path):
|
|
100
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
101
|
+
manifest = json.load(f)
|
|
102
|
+
|
|
103
|
+
library = {
|
|
104
|
+
"source": str(manifest_path),
|
|
105
|
+
"scenes": sorted(manifest.get("scenes", [])),
|
|
106
|
+
"characters": sorted(
|
|
107
|
+
manifest.get("characters", []) + manifest.get("extracharacters", [])
|
|
108
|
+
),
|
|
109
|
+
"expressions": sorted(
|
|
110
|
+
a[len(EXPRESSION_PREFIX):]
|
|
111
|
+
for a in manifest.get("dialogue", [])
|
|
112
|
+
if a.startswith(EXPRESSION_PREFIX)
|
|
113
|
+
),
|
|
114
|
+
"voices": sorted(manifest.get("voices", [])),
|
|
115
|
+
"music": sorted(manifest.get("music", [])),
|
|
116
|
+
"sounds": sorted(manifest.get("sounds", [])),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
scenes_dir = Path(manifest_path).parents[2] / "Content" / "scenes"
|
|
120
|
+
if scenes_dir.is_dir():
|
|
121
|
+
library["scene_positions"] = extract_scene_positions(scenes_dir)
|
|
122
|
+
|
|
123
|
+
animation_db = (
|
|
124
|
+
Path(manifest_path).parents[2] / "Animation" / "Standalone"
|
|
125
|
+
/ "AnimationMetaDatabase.animationdata"
|
|
126
|
+
)
|
|
127
|
+
if animation_db.exists():
|
|
128
|
+
library["actions"] = extract_actions(animation_db, set(library["expressions"]))
|
|
129
|
+
return library
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
manifest_path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_MANIFEST
|
|
134
|
+
if not manifest_path.exists():
|
|
135
|
+
sys.exit("Manifeste introuvable: %s" % manifest_path)
|
|
136
|
+
|
|
137
|
+
library = build_library(manifest_path)
|
|
138
|
+
DATA_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
with open(DATA_PATH, "w", encoding="utf-8") as f:
|
|
140
|
+
json.dump(library, f, ensure_ascii=False, indent=2)
|
|
141
|
+
|
|
142
|
+
for key in ("scenes", "characters", "expressions", "voices", "music", "sounds"):
|
|
143
|
+
print("%-12s %d" % (key, len(library[key])))
|
|
144
|
+
positions = library.get("scene_positions", {})
|
|
145
|
+
print("%-12s %d scenes (%d slots)" % (
|
|
146
|
+
"positions", len(positions), sum(len(v) for v in positions.values())
|
|
147
|
+
))
|
|
148
|
+
print("%-12s %d" % ("actions", len(library.get("actions", {}))))
|
|
149
|
+
print("OK ->", DATA_PATH)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|