#!/usr/bin/env python3 """ destroyed.py - regenerate the .data and .video of a *_destroyed mech variant. python3 destroyed.py [--erf X.erf] [--obb X.obb] [-o outDir] python3 destroyed.py --verify # regenerate all 89 of ours and diff A destroyed variant is four files: .data text, DeathEntity boilerplate .video text, four-page render graph .erf geometry, verbatim in the package _SOLID.obb collision, verbatim in the package The .erf and .obb come straight out of the package. The .data and .video are compiled away (a 12-byte stub plus {Element}/{GameModel}, and a binary element tree with embedded #FRE/#RLM blobs), so they are regenerated from a template. That is safe here because the template is invariant. Across all 89 destroyed variants in our own Content tree, every one of Class, OBBCollides, Collider, CanBeShot, CanBeWalkedOn, VertexLighting, FaceLighting, LookupLighting, LightMapLighting and CraterName is identical. The only things that vary are the three filename references, and those are read from the files actually present rather than guessed - which also preserves V4H's own `champion_stroyed` typo, since that is the name their build really uses. The .data and .video each appear in two forms in our tree, differing only by a trailing blank line; the majority form is emitted. """ import sys, os, re, glob, argparse, collections DATA_TEMPLATE = ( "[GameData]\r\n" "Class=MechWarrior4::DeathEntity\r\n" "SolidOBB={obb}\r\n" "OBBCollides=false\r\n" "Collider=false\r\n" "CanBeShot=false\r\n" "CanBeWalkedOn=false\r\n" "VertexLighting=yes\r\n" "FaceLighting=yes\r\n" "LookupLighting=no\r\n" "LightMapLighting=no\r\n" "CraterName=crater1\r\n" "\r\n" "[Renderers]\r\n" "VideoRenderer={video}\r\n" "\r\n" ) VIDEO_TEMPLATE = ( "[lod]\r\n" "Type=ShapeComponent\r\n" "Geometry={erf}\r\n" "\r\n" "[watcher]\r\n" "Type=AttributeWatcherOfInt\r\n" "Attribute=VisualRepresentation\r\n" "SimulationShouldExecute=1\r\n" "\r\n" "[damageappearance]\r\n" "Type=SwitchComponent\r\n" "Input=Watcher\r\n" "Child=LOD\r\n" "\r\n" "[locator]\r\n" "Child=DamageAppearance\r\n" "\r\n" ) OUR_MECH_SOURCE = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs" def find_parts(mech_dir, compiled_dir=None): """-> (stem, erfName, obbName). Looks in mech_dir, then the compiled tree.""" erfs = [f for f in os.listdir(mech_dir) if f.lower().endswith(".erf")] obbs = [f for f in os.listdir(mech_dir) if f.lower().endswith(".obb")] stem = None if compiled_dir and os.path.isdir(compiled_dir): datas = [f for f in os.listdir(compiled_dir) if f.lower().endswith(".data")] if datas: stem = datas[0][:-len(".data")] if stem is None and obbs: stem = re.sub(r"_solid\.obb$", "", obbs[0], flags=re.I) stem = re.sub(r"\.obb$", "", stem, flags=re.I) return stem, (erfs[0] if erfs else None), (obbs[0] if obbs else None) def generate(stem, erf, obb): return (DATA_TEMPLATE.format(obb=obb, video=f"{stem}.video"), VIDEO_TEMPLATE.format(erf=erf)) def verify(): ok = collections.Counter() problems = [] for src_dir in sorted(glob.glob(OUR_MECH_SOURCE + "/*_[Dd]estroyed") + glob.glob(OUR_MECH_SOURCE + "/*_DESTROYED")): datas = glob.glob(src_dir + "/*.data") videos = glob.glob(src_dir + "/*.video") if not datas or not videos: continue stem = os.path.basename(datas[0])[:-len(".data")] want_data = open(datas[0], "rb").read().decode("latin-1") want_video = open(videos[0], "rb").read().decode("latin-1") kv = dict(re.findall(r'^([A-Za-z0-9_]+)=([^\r\n]*)', want_data, re.M)) erf = re.search(r'Geometry=([^\r\n]*)', want_video).group(1).strip() got_data, got_video = generate(stem, erf, kv["SolidOBB"].strip()) ok["files"] += 1 for what, want, got, exact_key, soft_key in ( (".data", want_data, got_data, "data_exact", "data_soft"), (".video", want_video, got_video, "video_exact", "video_soft")): if got == want: ok[exact_key] += 1 elif got.rstrip("\r\n").lower() == want.rstrip("\r\n").lower(): # NotationFile compares with _stricmp throughout, and the sources # themselves are inconsistent (one uses [renderers], others # [Renderers]), so case and a trailing blank line are cosmetic. ok[soft_key] += 1 else: problems.append((os.path.basename(src_dir), what, want, got)) print(f"destroyed variants checked : {ok['files']}") print(f" .data equivalent : {ok['data_exact'] + ok['data_soft']}" f" (byte-exact {ok['data_exact']}, +{ok['data_soft']} case / trailing-blank-line only)") print(f" .video equivalent : {ok['video_exact'] + ok['video_soft']}" f" (byte-exact {ok['video_exact']}, +{ok['video_soft']} case / trailing-blank-line only)") if problems: print(f"\n{len(problems)} real differences:") for name, what, want, got in problems[:6]: print(f" {name} {what}") for w, g in zip(want.splitlines(), got.splitlines()): if w != g: print(f" source={w!r}\n got ={g!r}") def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("mechdir", nargs="?", help="the *_destroyed folder holding .erf/.obb") ap.add_argument("--compiled", help="matching folder in the _compiled tree, for the stem") ap.add_argument("-o", "--out", help="output folder (defaults to mechdir)") ap.add_argument("--verify", action="store_true") args = ap.parse_args() if args.verify: verify() return if not args.mechdir: ap.error("give a *_destroyed folder, or --verify") stem, erf, obb = find_parts(args.mechdir, args.compiled) if not (stem and erf and obb): sys.exit(f"{args.mechdir}: need a stem, an .erf and an .obb " f"(got stem={stem!r} erf={erf!r} obb={obb!r})") data, video = generate(stem, erf, obb) out = args.out or args.mechdir os.makedirs(out, exist_ok=True) open(os.path.join(out, f"{stem}.data"), "wb").write(data.encode("latin-1")) open(os.path.join(out, f"{stem}.video"), "wb").write(video.encode("latin-1")) print(f"{os.path.basename(args.mechdir)}: {stem}.data + {stem}.video " f"(SolidOBB={obb}, Geometry={erf})") if __name__ == "__main__": main()