Skip to content

Commit d5ef3fa

Browse files
committed
Merge branch 'tail/cut' into tail/attach
2 parents 297f501 + 09acbd8 commit d5ef3fa

26 files changed

Lines changed: 385 additions & 166 deletions

‎apps/studio/src/mhfu_studio/monster/clips.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,16 +179,16 @@ def coverage(
179179
port: AnimPack,
180180
host: AnimPack | None = None,
181181
donor: Mapping[int, anim.Clip] | None = None,
182-
layout: Mapping[int, int] | None = None,
182+
layout: layouts.Layout | None = None,
183183
) -> Coverage:
184-
"""Each entry of a built pack against the donor clip `layout` (entry -> MHP3rd id; by
185-
default each entry read as the clip of its own id) puts there and the host's pack; every
186-
entry is UNKNOWN without the donor."""
184+
"""Each entry of a built pack against the donor clip `layout` puts there (by default each
185+
entry read as the clip of its own id) and the host's pack; every entry is UNKNOWN without
186+
the donor."""
187187
table = clip_table(port)
188188
host_table = clip_table(host) if host is not None else {}
189189
donor = donor or {}
190190
if layout is None:
191-
layout = {cid: cid for cid in donor if cid in table}
191+
layout = layouts.Layout({cid: cid for cid in donor if cid in table})
192192
held = slots.correspondence(port, donor, layout) if donor else {}
193193
cov = Coverage(has_source=bool(donor), has_host=host is not None)
194194
for slot, fp in sorted(table.items()):
@@ -201,11 +201,11 @@ def coverage(
201201
kind = FILLER
202202
else:
203203
kind = HOST if host_table.get(slot) == fp else ALTERED
204-
cid = layout.get(slot)
204+
cid = layout.entries.get(slot)
205205
fp_src = fingerprint(donor[cid]) if cid is not None and cid in donor else None
206206
cov.slots[slot] = SlotCoverage(slot, kind, fp[0], fp[1], fp_src, cid)
207207
if host is not None:
208-
placed = set(layout.values())
208+
placed = set(layout.entries.values())
209209
cov.dropped = {cid: fingerprint(c) for cid, c in donor.items() if cid not in placed}
210210
return cov
211211

‎apps/studio/src/mhfu_studio/monster/commands.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def run_clips(args: argparse.Namespace) -> int:
267267
port = slots.anim_of(b.pac)
268268
table = clips.clip_table(port)
269269
host, donor = inputs.host_anim(m, games), inputs.donor_clips(m, games)
270-
cov = clips.coverage(port, host, donor, layout.of(m, donor, host).entries)
270+
cov = clips.coverage(port, host, donor, layout.of(m, donor, host))
271271
if args.import_labels:
272272
labels = slots.read_labels(args.import_labels.read_text(encoding="utf-8"))
273273
provenance = args.labels_from or clips.UNRECORDED.format(args.import_labels.name)

‎apps/studio/src/mhfu_studio/monster/validate.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ def own_moves(
466466
refused += _refused(dataclasses.replace(m, moves={name: mv}, rules=[]), lay, known)
467467
out += refused or _refused(dataclasses.replace(m, moves=judged), lay, known)
468468
for name, mv in mine:
469-
e = lay.ids.get(m.clips[mv.clip].id) if mv.clip is not None else mv.anim
469+
e = lay.entry(m.clips[mv.clip]) if mv.clip is not None else mv.anim
470470
frames = lay.frames.get(e) if e is not None else None
471471
for i, a in enumerate(mv.attacks):
472472
if frames is not None and a.frame < frames and a.end is not None and a.end > frames:

‎apps/studio/src/mhfu_studio/monster/workspace.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -970,7 +970,7 @@ def _measure_coverage(self) -> tuple[clips.Coverage, list[str]]:
970970
notes.append(f"original moveset unreadable ({e})")
971971
if host is not None and donor is not None:
972972
try:
973-
placed = layout.of(m, donor, host).entries
973+
placed = layout.of(m, donor, host)
974974
except ValueError as e:
975975
notes.append(f"no clip layout ({e}): each anim read as its own clip id")
976976
return clips.coverage(port, host, donor, placed), notes

‎apps/studio/tests/monster/test_clips.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ def packs(make_clip):
4949

5050
def test_coverage(packs):
5151
port, host, donor = packs
52-
cov = C.coverage(port, host, donor, {1: 1, 2: 2, 5: 5, 6: 6})
52+
cov = C.coverage(port, host, donor, layout.Layout({1: 1, 2: 2, 5: 5, 6: 6}))
5353
kinds = {s: c.kind for s, c in cov.slots.items()}
5454
assert kinds == {1: C.CARRIED, 2: C.CARRIED, 3: C.FILLER, 4: C.HOST, 5: C.ALTERED, 6: C.CARRIED}
5555
assert cov.dropped == {7: (90, False)} and cov.slots[2].scriptable
5656
assert cov.counts()[C.DROPPED] == 1 and "1 donor clip(s) DROPPED" in cov.summary()
5757
assert "idle copy" in cov.slots[3].why().lower()
5858
assert cov.sources() == {1: 1, 2: 2, 5: 5, 6: 6}
59-
moved = C.coverage(port, host, donor, {2: 6, 6: 2})
59+
moved = C.coverage(port, host, donor, layout.Layout({2: 6, 6: 2}))
6060
assert (moved.kind(2), moved.kind(6), moved.slots[2].clip) == (C.ALTERED, C.FILLER, 6)
6161

6262

@@ -202,11 +202,15 @@ def test_zinogre(games, built, ports):
202202
m = manifest.load(ports / "zinogre.toml")
203203
port = slots.anim_of(built("zinogre"))
204204
host, donor = host_anim(m, games), donor_clips(m, games)
205-
cov = C.coverage(port, host, donor, layout.of(m, donor, host).entries)
205+
cov = C.coverage(port, host, donor, layout.of(m, donor, host))
206206
n = cov.counts()
207-
assert len(cov.slots) == n[C.CARRIED] == 102 and n[C.FILLER] == n[C.DROPPED] == 0
207+
assert len(cov.slots) == n[C.CARRIED] == 103 and n[C.FILLER] == n[C.DROPPED] == 0
208208
assert all(t.trusted for t in C.track_labels(m, C.clip_table(port), sources=cov.sources()))
209-
assert {t.status for t in C.track_labels(m, C.clip_table(port))} == {C.UNCHECKABLE}
209+
pinned = {name for name, c in m.clips.items() if c.slot is not None}
210+
got = {t.name: t.status for t in C.track_labels(m, C.clip_table(port))}
211+
assert {k: v for k, v in got.items() if v != C.UNCHECKABLE} == dict.fromkeys(
212+
pinned, C.STILL_VALID
213+
), "without a layout only a pin is checkable"
210214

211215

212216
def test_brute(built):

‎apps/studio/tests/monster/test_commands.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def test_clips(games, ports, tmp_path, capsys):
9191
data = ["--data", str(games.fu.root), "--p3rd-data", str(games.p3rd.root)]
9292
assert main(["port", "clips", str(ports / "zinogre.toml"), *data]) == 0
9393
out = capsys.readouterr().out
94-
assert "102 populated slot(s): 102 carried, 0 filler" in out and "DROPPED" not in out
94+
assert "103 populated slot(s): 103 carried, 0 filler" in out and "DROPPED" not in out
9595
assert main(["port", "clips", str(ports / "zinogre.toml"), "--slots", *data]) == 0
9696
assert "a1 2 CARRIED welcome_howl" in capsys.readouterr().out
9797
path = tmp_path / "z.toml"

‎apps/studio/tests/monster/test_scene.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66
import pytest
77
from mhfu.files import monster_pac
8-
from mhfu_port import layout, manifest, travel, verify
8+
from mhfu_port import fk, layout, manifest, motion, travel, verify
99
from mhfu_port.model import MHFU, MHP3RD, ModelError
1010
from mhfu_studio.monster import inputs
1111
from mhfu_studio.monster.core.scene import Scene, open_scene
@@ -89,7 +89,9 @@ def test_donor_matches_port(games, built, ports, name, joints, groups, vertices,
8989
src = Scene.from_manifest(m, side="source", data=games)
9090
port = Scene.from_bytes(built(name), name, manifest=m)
9191
host = inputs.host_anim(m, games)
92-
entry = layout.of(m, inputs.donor_clips(m, games), host).ids
92+
lay = layout.of(m, inputs.donor_clips(m, games), host)
93+
played = layout.clips(lay, {c.slot: c.source for c in src.clips})
94+
tracks = {c.slot: c.joint_tracks for c in src.clips}
9395
assert src.game == MHP3RD and src.rig.n == joints and src.record_to_bone
9496
assert (
9597
(len(src.groups), src.n_vertices)
@@ -109,24 +111,25 @@ def test_donor_matches_port(games, built, ports, name, joints, groups, vertices,
109111
s_idx = np.array(sorted(b for b, j in c.tree.items() if j not in (0, root)))
110112
p_idx = np.array([c.tree[i] for i in s_idx])
111113
worst, compared = 0.0, 0
112-
for clip in src.clips:
113-
pc = port._by_slot[entry[clip.slot]]
114+
for e, clip in played.items():
115+
pc = port._by_slot[e]
114116
assert pc.whole_rig
115117
turn = turn_of(port, pc)
116118
th = -turn.keys[0] / travel.TURN * math.tau if turn else 0.0
117119
face = np.array(
118120
[[math.cos(th), 0, -math.sin(th)], [0, 1, 0], [math.sin(th), 0, math.cos(th)]]
119121
)
120-
for frame in (clip.frames // 4, clip.frames // 2):
121-
a = src.pose(clip, frame).joints @ face
122+
want, n = fk.Curves(clip, src.rig, tracks[lay.entries[e]]), motion.frames(clip)
123+
for frame in (n // 4, n // 2):
124+
a = src.rig.world(*want.at(float(frame)))[:, :3, 3] @ face
122125
b = pose_at(port, pc, frame).joints
123126
lift = b[body, 1] - a[anchor, 1]
124127
off = (a[s_idx] - a[anchor]) - (b[p_idx] - b[body])
125128
worst = max(
126129
worst, float(np.linalg.norm(off, axis=1).max()), abs(lift - m.build.ground_lift)
127130
)
128131
compared += 1
129-
assert compared == 2 * len(src.clips) and worst < 0.2, (compared, worst)
132+
assert compared == 2 * len(played) and worst < 0.2, (compared, worst)
130133

131134

132135
def test_manifest_port(games, ports):

‎framework/tests/test_examples.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,10 @@ def test_ported_zinogre_dashes_at_a_far_hunter(lua: Any, tmp_path: Path) -> None
172172

173173

174174
def zinogre_clips() -> dict[str, int]:
175-
"""The entries `mhfu-port inject` writes to zinogre_clips.lua: a clip below the host's 123
176-
entries sits in the entry of its MHP3rd id, which the manifest gives as `source`."""
175+
"""The entries `mhfu-port inject` writes to zinogre_clips.lua: a pinned clip's `slot`, else
176+
the entry of its MHP3rd id `source`, where a clip below the host's 123 entries sits."""
177177
clips = tomllib.loads(PORT.read_text(encoding="utf-8"))["clips"]
178-
return {name: clips[name]["source"] for name in FOLLOWER_CLIPS}
178+
return {name: clips[name].get("slot", clips[name].get("source")) for name in FOLLOWER_CLIPS}
179179

180180

181181
CLIPS = zinogre_clips()

‎packages/mhfu-port/src/mhfu_port/build.py‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,10 @@ def animation(
230230
placed: Layout,
231231
lift: float = 0.0,
232232
) -> fu.Anim:
233-
"""The donor's moveset on the rig's joints in the entries `placed` gives it, its pelvis
234-
raised by `lift` world units, and on its own rig its travel and turns where the engine takes
235-
them (`travel.carry`). Entries it leaves keep the host's clips on the host's rig."""
233+
"""The donor's moveset on the rig's joints in the entries `placed` gives it, cut where it
234+
cuts, its pelvis raised by `lift` world units, and on its own rig its travel and turns where
235+
the engine takes them (`travel.carry`). Entries it leaves keep the host's clips on the
236+
host's rig."""
236237
clips: Mapping[int, Clip] = d.clips
237238
if lift:
238239
bone_of_record = {r: bone for bone, r in record_of.items()}
@@ -243,17 +244,19 @@ def animation(
243244
# records are not positional in either mode: joint -> donor bone -> record
244245
track_of = {j: record_of.get(bone) for j, bone in bind.bone_of.items() if bone is not None}
245246
keep = bind.mode == "retarget"
246-
out = motion.build(clips, placed.entries, h.anim, bind.rig.streams, track_of, keep)
247+
played = layouts.clips(placed, clips)
248+
out = motion.build(played, {e: e for e in played}, h.anim, bind.rig.streams, track_of, keep)
247249
return out if keep else travel.carry(out, bind.rig.skeleton)
248250

249251

250252
def authored(m: Manifest, placed: Layout) -> dict[int, float]:
251253
"""Entry -> the manifest's `turn` of the clip it holds."""
252-
return {
253-
placed.ids[c.id]: c.turn
254-
for c in m.clips.values()
255-
if c.turn is not None and c.id in placed.ids
256-
}
254+
out = {}
255+
for c in m.clips.values():
256+
e = placed.entry(c)
257+
if c.turn is not None and e is not None:
258+
out[e] = c.turn
259+
return out
257260

258261

259262
# 6. the PAC
@@ -284,7 +287,7 @@ def build(m: Manifest, data: Data) -> Built:
284287
placed = layout(m, d, h)
285288
anim = animation(d, h, bind, record_of, placed, m.build.ground_lift)
286289
turned = travel.turns(anim, bind.rig.skeleton, authored(m, placed))
287-
frames = {e: motion.frames(d.clips[cid]) for e, cid in placed.entries.items()}
290+
frames = {e: motion.frames(c) for e, c in layouts.clips(placed, d.clips).items()}
288291
placed = dataclasses.replace(placed, turns=turned, frames=frames)
289292
out = pac(h, bind.rig, model, d.textures, anim)
290293
summary = Summary(

‎packages/mhfu-port/src/mhfu_port/cli/slots.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ def _resolve(args: argparse.Namespace) -> manifest.Manifest | None:
5858

5959
def _layout(
6060
m: manifest.Manifest | None, donor: Mapping[int, Clip], host: fu.Anim, species: int
61-
) -> dict[int, int]:
61+
) -> layout.Layout:
6262
"""The manifest's layout of the donor's clips, else the packer's alone."""
6363
clips = m.clips if m is not None else {}
64-
return layout.plan(clips, donor, host, species).entries
64+
return layout.plan(clips, donor, host, species)
6565

6666

6767
def run_slots(args: argparse.Namespace) -> int:
@@ -72,7 +72,7 @@ def run_slots(args: argparse.Namespace) -> int:
7272
port = slots.anim_of(args.port.read_bytes()) if args.port else None
7373
donor = motion.moveset(d.p3rd.read(args.donor)) if args.donor is not None else None
7474
placed = _layout(m, donor, host, args.host) if donor is not None else None
75-
ids = {cid: e for e, cid in (placed or {}).items()}
75+
ids = placed.ids if placed is not None else {}
7676
named = sorted(m.clips.items()) if m is not None else []
7777
names = {e: n for n, c in named if (e := layout.where(c, ids)) is not None}
7878
rows = slots.catalog(host, drivers, port, donor, placed, names)

0 commit comments

Comments
 (0)