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.
@@ -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()