Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit b3959a2

Browse files
committed
Version 0.1.7
Support custom typetrees
1 parent 73fd954 commit b3959a2

4 files changed

Lines changed: 125 additions & 114 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ dist/
44
__pycache__/
55
*.egg-info
66
dump
7+
test_*

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ General purpose [Live2D](https://www.live2d.com/) Asset recovery tool built w/ [
88
As the name suggests, this project is heavily inspired by [Perfare/UnityLive2DExtractor](https://github.com/Perfare/UnityLive2DExtractor). With a few key differences:
99
- All Live2D types are implemented with [dumped TypeTree](https://github.com/mos9527/UnityPyLive2DExtractor/blob/main/external/typetree_cubism.json) and [generated types](https://github.com/mos9527/UnityPyLive2DExtractor/blob/main/typetree_codegen.py). This should help with compatibility issues.
1010
- Do note, however, that you may need to update the TypeTree if the Live2D version changes.
11-
- Generate the TypeTree with [typetree_codegen](https://github.com/mos9527/UnityPyLive2DExtractor/blob/main/typetree_codegen.py) and replace the existing TypeTree at `UnityPyLive2DExtractor/generated`
11+
- Generate the TypeTree with [typetree_codegen](https://github.com/mos9527/UnityPyLive2DExtractor/blob/main/typetree_codegen.py)
1212
```bash
13-
python typetree_codegen.py type_tree_cubism.json UnityPyLive2DExtractor/generated
13+
python typetree_codegen.py type_tree_cubism.json l2d_generated
14+
```
15+
- You can specify the TypeTree module to use. Make sure the generated folder is available in the current directory.
16+
```bash
17+
UnityPyLive2DExtractor --module l2d_generated <input> <output>
1418
```
15-
- Feel free to submit a PR into a new branch if you have found a new TypeTree that worked for you.
1619
- New (not necessarily better) asset discovery method. Though proven to be more reliable in some cases.
1720

1821
## Installation

UnityPyLive2DExtractor/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = (0, 1, 6)
1+
__version__ = (0, 1, 7)

UnityPyLive2DExtractor/__main__.py

Lines changed: 117 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import argparse
2-
import os, io, json
2+
import os, io, json, importlib
33
from zlib import crc32
44
import UnityPy
55
from typing import TypeVar
@@ -21,8 +21,13 @@
2121
# TypeTreeHelper.read_typetree_boost = False
2222
logger = getLogger("UnityPyLive2DExtractor")
2323

24+
from sssekai.fmt.motion3 import to_motion3
25+
from sssekai.fmt.moc3 import read_moc3
26+
from sssekai.unity.AnimationClip import AnimationHelper
27+
2428
from UnityPyLive2DExtractor import __version__
25-
from UnityPyLive2DExtractor.generated import TYPETREE_DEFS
29+
30+
# These are here ONLY for the sake of type hints
2631
from UnityPyLive2DExtractor.generated.Live2D.Cubism.Core import CubismModel
2732
from UnityPyLive2DExtractor.generated.Live2D.Cubism.Rendering import CubismRenderer
2833
from UnityPyLive2DExtractor.generated.Live2D.Cubism.Framework.Physics import (
@@ -35,135 +40,133 @@
3540
CubismPhysicsRig,
3641
CubismPhysicsController,
3742
)
38-
from sssekai.fmt.motion3 import to_motion3
39-
from sssekai.fmt.moc3 import read_moc3
40-
from sssekai.unity.AnimationClip import AnimationHelper
41-
4243

43-
def monkey_patch(cls):
44-
"""ooh ooh aah aah"""
45-
46-
def wrapper(func):
47-
setattr(cls, func.__name__, func)
48-
return func
44+
from dataclasses import dataclass
4945

50-
return wrapper
5146

47+
@dataclass
48+
class ExtractorConfig:
49+
generated_module: str = "UnityPyLive2DExtractor.generated"
5250

53-
@monkey_patch(CubismPhysicsNormalizationTuplet)
54-
def dump(self: CubismPhysicsNormalizationTuplet):
55-
return {
56-
"Maximum": self.Maximum,
57-
"Minimum": self.Minimum,
58-
"Default": self.Default,
59-
}
51+
@property
52+
def module(self):
53+
return __import__(self.generated_module)
6054

55+
def import_path(self, fullname: str):
56+
namespace = ".".join(fullname.split(".")[:-1])
57+
classname = fullname.split(".")[-1]
58+
cls = importlib.import_module(f".{namespace}", package=self.generated_module)
59+
cls = getattr(cls, classname)
60+
return cls
6161

62-
@monkey_patch(CubismPhysicsNormalization)
63-
def dump(self: CubismPhysicsNormalization):
64-
return {"Position": self.Position.dump(), "Angle": self.Angle.dump()}
6562

63+
CFG = ExtractorConfig()
6664

67-
@monkey_patch(CubismPhysicsParticle)
68-
def dump(self: CubismPhysicsParticle):
69-
return {
70-
"Position": {"X": self.InitialPosition.x, "Y": self.InitialPosition.y},
71-
"Mobility": self.Mobility,
72-
"Delay": self.Delay,
73-
"Acceleration": self.Acceleration,
74-
"Radius": self.Radius,
75-
}
7665

66+
def patch_all():
67+
def monkey_patch(fullname):
68+
"""ooh ooh aah aah"""
69+
cls = CFG.import_path(fullname)
7770

78-
@monkey_patch(CubismPhysicsOutput)
79-
def dump(self: CubismPhysicsOutput):
80-
return {
81-
"Destination": {"Target": "Parameter", "Id": self.DestinationId},
82-
"VertexIndex": self.ParticleIndex,
83-
"Scale": self.AngleScale,
84-
"Weight": self.Weight,
85-
"Type": ["X", "Y", "Angle"][self.SourceComponent],
86-
"Reflect": self.IsInverted,
87-
}
71+
def wrapper(func):
72+
setattr(cls, func.__name__, func)
73+
return func
8874

75+
return wrapper
8976

90-
@monkey_patch(CubismPhysicsInput)
91-
def dump(self: CubismPhysicsInput):
92-
return {
93-
"Source": {"Target": "Parameter", "Id": self.SourceId},
94-
"Weight": self.Weight,
95-
"Type": ["X", "Y", "Angle"][self.SourceComponent],
96-
"Reflect": self.IsInverted,
97-
}
77+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsNormalizationTuplet")
78+
def dump(self: CubismPhysicsNormalizationTuplet):
79+
return {
80+
"Maximum": self.Maximum,
81+
"Minimum": self.Minimum,
82+
"Default": self.Default,
83+
}
9884

85+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsNormalization")
86+
def dump(self: CubismPhysicsNormalization):
87+
return {"Position": self.Position.dump(), "Angle": self.Angle.dump()}
88+
89+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsParticle")
90+
def dump(self: CubismPhysicsParticle):
91+
return {
92+
"Position": {"X": self.InitialPosition.x, "Y": self.InitialPosition.y},
93+
"Mobility": self.Mobility,
94+
"Delay": self.Delay,
95+
"Acceleration": self.Acceleration,
96+
"Radius": self.Radius,
97+
}
9998

100-
@monkey_patch(CubismPhysicsSubRig)
101-
def dump(self: CubismPhysicsSubRig):
102-
return {
103-
"Input": [x.dump() for x in self.Input],
104-
"Output": [x.dump() for x in self.Output],
105-
"Vertices": [x.dump() for x in self.Particles],
106-
"Normalization": self.Normalization.dump(),
107-
}
99+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsOutput")
100+
def dump(self: CubismPhysicsOutput):
101+
return {
102+
"Destination": {"Target": "Parameter", "Id": self.DestinationId},
103+
"VertexIndex": self.ParticleIndex,
104+
"Scale": self.AngleScale,
105+
"Weight": self.Weight,
106+
"Type": ["X", "Y", "Angle"][self.SourceComponent],
107+
"Reflect": self.IsInverted,
108+
}
108109

110+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsInput")
111+
def dump(self: CubismPhysicsInput):
112+
return {
113+
"Source": {"Target": "Parameter", "Id": self.SourceId},
114+
"Weight": self.Weight,
115+
"Type": ["X", "Y", "Angle"][self.SourceComponent],
116+
"Reflect": self.IsInverted,
117+
}
109118

110-
@monkey_patch(CubismPhysicsRig)
111-
def dump(self: CubismPhysicsRig):
112-
return [
113-
{"Id": "PhysicsSetting%d" % (i + 1), **rig.dump()}
114-
for i, rig in enumerate(self.SubRigs)
115-
]
119+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsSubRig")
120+
def dump(self: CubismPhysicsSubRig):
121+
return {
122+
"Input": [x.dump() for x in self.Input],
123+
"Output": [x.dump() for x in self.Output],
124+
"Vertices": [x.dump() for x in self.Particles],
125+
"Normalization": self.Normalization.dump(),
126+
}
116127

128+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsRig")
129+
def dump(self: CubismPhysicsRig):
130+
return [
131+
{"Id": "PhysicsSetting%d" % (i + 1), **rig.dump()}
132+
for i, rig in enumerate(self.SubRigs)
133+
]
117134

118-
@monkey_patch(CubismPhysicsController)
119-
def dump(self: CubismPhysicsController):
120-
return {
121-
"Version": 3,
122-
"Meta": {
123-
"PhysicsSettingCount": len(self._rig.SubRigs),
124-
"TotalInputCount": sum((len(x.Input) for x in self._rig.SubRigs)),
125-
"TotalOutputCount": sum((len(x.Output) for x in self._rig.SubRigs)),
126-
"VertexCount": sum((len(x.Particles) for x in self._rig.SubRigs)),
127-
"Fps": 60,
128-
"EffectiveForces": {
129-
"Gravity": {"X": 0, "Y": -1},
130-
"Wind": {"X": 0, "Y": 0},
135+
@monkey_patch("Live2D.Cubism.Framework.Physics.CubismPhysicsController")
136+
def dump(self: CubismPhysicsController):
137+
return {
138+
"Version": 3,
139+
"Meta": {
140+
"PhysicsSettingCount": len(self._rig.SubRigs),
141+
"TotalInputCount": sum((len(x.Input) for x in self._rig.SubRigs)),
142+
"TotalOutputCount": sum((len(x.Output) for x in self._rig.SubRigs)),
143+
"VertexCount": sum((len(x.Particles) for x in self._rig.SubRigs)),
144+
"Fps": 60,
145+
"EffectiveForces": {
146+
"Gravity": {"X": 0, "Y": -1},
147+
"Wind": {"X": 0, "Y": 0},
148+
},
149+
"PhysicsDictionary": [
150+
{"Id": "PhysicsSetting%d" % (i + 1), "Name": "%d" % (i + 1)}
151+
for i, _ in enumerate(self._rig.SubRigs)
152+
],
131153
},
132-
"PhysicsDictionary": [
133-
{"Id": "PhysicsSetting%d" % (i + 1), "Name": "%d" % (i + 1)}
134-
for i, _ in enumerate(self._rig.SubRigs)
135-
],
136-
},
137-
"PhysicsSettings": self._rig.dump(),
138-
}
139-
140-
141-
@monkey_patch(CubismRenderer)
142-
def __hash__(self: CubismRenderer):
143-
return self._mainTexture.path_id
144-
145-
146-
@monkey_patch(CubismRenderer)
147-
def __eq__(self: CubismRenderer, other: CubismRenderer):
148-
return self.__hash__() == other.__hash__()
149-
150-
151-
from dataclasses import dataclass
152-
153-
154-
@dataclass
155-
class ExtractorFlags:
156-
live2d_variant: str = "cubism"
154+
"PhysicsSettings": self._rig.dump(),
155+
}
157156

157+
@monkey_patch("Live2D.Cubism.Rendering.CubismRenderer")
158+
def __hash__(self: CubismRenderer):
159+
return self._mainTexture.path_id
158160

159-
FLAGS = ExtractorFlags()
161+
@monkey_patch("Live2D.Cubism.Rendering.CubismRenderer")
162+
def __eq__(self: CubismRenderer, other: CubismRenderer):
163+
return self.__hash__() == other.__hash__()
160164

161165

162166
# XXX: Is monkey patching this into UnityPy a good idea?
163167
def read_from(reader: ObjectReader, **kwargs):
164168
"""Import generated classes by MonoBehavior script class type and read from reader"""
165-
import importlib
166-
169+
TYPETREE_DEFS = CFG.import_path("TYPETREE_DEFS")
167170
match reader.type:
168171
case ClassIDType.MonoBehaviour:
169172
mono: MonoBehaviour = reader.read(check_read=False)
@@ -178,10 +181,7 @@ def read_from(reader: ObjectReader, **kwargs):
178181

179182
if typetree:
180183
result = reader.read_typetree(typetree)
181-
nameSpace = importlib.import_module(
182-
f".generated.{nameSpace}", package="UnityPyLive2DExtractor"
183-
)
184-
clazz = getattr(nameSpace, className, None)
184+
clazz = CFG.import_path(fullName)
185185
instance = clazz(object_reader=reader, **result)
186186
return instance
187187
else:
@@ -201,6 +201,11 @@ def __main__():
201201
)
202202
parser.add_argument("infile", help="Input file/directory to extract from")
203203
parser.add_argument("outdir", help="Output directory to extract to")
204+
parser.add_argument(
205+
"--module",
206+
help="Generated Live2D module from TypeTree dump. Read the README for info.",
207+
default="UnityPyLive2DExtractor.generated",
208+
)
204209
parser.add_argument(
205210
"--log-level",
206211
help="Set logging level",
@@ -216,8 +221,10 @@ def __main__():
216221
fmt="%(asctime)s %(name)s [%(levelname).4s] %(message)s",
217222
isatty=True,
218223
)
219-
os.makedirs(args.outdir, exist_ok=True)
220224
logger.info("UnityPyLive2D Extractor v%d.%d.%d" % __version__)
225+
CFG.generated_module = args.module
226+
patch_all()
227+
os.makedirs(args.outdir, exist_ok=True)
221228
logger.info("Loading %s" % args.infile)
222229
env = UnityPy.load(args.infile)
223230
objs = [

0 commit comments

Comments
 (0)