Finished with Phase 2.6
This commit is contained in:
@@ -149,6 +149,232 @@ def _summarize_animdef(filename, data):
|
||||
}
|
||||
|
||||
|
||||
def read_pack_meta(path):
|
||||
"""Read the animationframework block from pack.mcmeta regardless of whether
|
||||
the pack has animdefs (models/textures packs). None when absent."""
|
||||
zf = _open_zip(path)
|
||||
if zf is None:
|
||||
return None
|
||||
try:
|
||||
try:
|
||||
mcmeta = _decode(zf.read('pack.mcmeta'))
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
return None
|
||||
af = mcmeta.get('animationframework')
|
||||
if not af:
|
||||
return None
|
||||
authors = []
|
||||
if af.get('author'):
|
||||
authors = [a.strip() for a in str(af['author']).split(',') if a.strip()]
|
||||
return {
|
||||
'animation_id': af.get('id') or None,
|
||||
'name': str(af.get('name') or ''),
|
||||
'version': str(af.get('version') or ''),
|
||||
'authors': authors,
|
||||
'description': str(af.get('description') or ''),
|
||||
}
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
|
||||
def _resource_to_member(resource):
|
||||
"""needsofnature:textures/entity/zombie/zombie.png → assets/needsofnature/..."""
|
||||
if ':' in resource:
|
||||
ns, _, path = resource.partition(':')
|
||||
return f'assets/{ns}/{path}'
|
||||
if resource.startswith('assets/'):
|
||||
return resource
|
||||
return f'assets/{resource}'
|
||||
|
||||
|
||||
def read_models_manifest(path):
|
||||
"""Build the Models/Textures manifest from GeckoLib geo models.
|
||||
|
||||
Returns None when the pack has no geo models. Each model records its geo
|
||||
filename, identifier, bone/cube counts, and the afw_bone_textures map
|
||||
(bone → texture zip member) so the viewer can texture it."""
|
||||
zf = _open_zip(path)
|
||||
if zf is None:
|
||||
return None
|
||||
try:
|
||||
names = set(zf.namelist())
|
||||
geo_names = sorted(
|
||||
n for n in names if '/geckolib/models/' in n and n.endswith('.geo.json')
|
||||
)
|
||||
if not geo_names:
|
||||
return None
|
||||
|
||||
models = []
|
||||
for name in geo_names:
|
||||
try:
|
||||
data = _decode(zf.read(name))
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
continue
|
||||
bone_textures = {}
|
||||
for bone, tex in (data.get('afw_bone_textures') or {}).items():
|
||||
member = _resource_to_member(str(tex))
|
||||
if member in names:
|
||||
bone_textures[str(bone)] = member
|
||||
bone_count = 0
|
||||
cube_count = 0
|
||||
identifier = ''
|
||||
for geom in data.get('minecraft:geometry') or []:
|
||||
identifier = identifier or (geom.get('description') or {}).get('identifier', '')
|
||||
for bone in geom.get('bones') or []:
|
||||
bone_count += 1
|
||||
cube_count += len(bone.get('cubes') or [])
|
||||
models.append({
|
||||
'name': name.rsplit('/', 1)[-1],
|
||||
'member': name,
|
||||
'identifier': identifier,
|
||||
'bones': bone_count,
|
||||
'cubes': cube_count,
|
||||
'bone_textures': bone_textures,
|
||||
'textures': sorted(set(bone_textures.values())),
|
||||
})
|
||||
|
||||
textures = sorted(n for n in names if n.startswith('assets/') and n.endswith('.png'))
|
||||
return {'models': models, 'textures': textures}
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
|
||||
def _item_name_from_member(member):
|
||||
parts = member.split('/')
|
||||
try:
|
||||
idx = parts.index('items')
|
||||
ns = parts[idx - 1]
|
||||
name = parts[idx + 1]
|
||||
if name.endswith('.json'):
|
||||
name = name[:-5]
|
||||
return f'{ns}:{name}'
|
||||
except (ValueError, IndexError):
|
||||
return member
|
||||
|
||||
|
||||
def _ingredient_names(recipe):
|
||||
if isinstance(recipe.get('key'), dict):
|
||||
raw = list(recipe['key'].values())
|
||||
elif 'ingredients' in recipe:
|
||||
raw = recipe['ingredients']
|
||||
else:
|
||||
raw = []
|
||||
names = set()
|
||||
for item in raw:
|
||||
if isinstance(item, dict):
|
||||
item = item.get('id')
|
||||
if item:
|
||||
names.add(str(item))
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def _result_name(recipe):
|
||||
res = recipe.get('result')
|
||||
if isinstance(res, dict):
|
||||
rid = res.get('id') or res.get('item') or ''
|
||||
count = res.get('count')
|
||||
if count and int(count) != 1:
|
||||
return f'{rid} x{count}'
|
||||
return str(rid)
|
||||
return str(res) if res else ''
|
||||
|
||||
|
||||
def read_logical_manifest(path):
|
||||
"""Build the Logical manifest outlining what a datapack adds (items,
|
||||
recipes, custom crafting, advancements, functions, NoN extras)."""
|
||||
zf = _open_zip(path)
|
||||
if zf is None:
|
||||
return None
|
||||
try:
|
||||
names = set(zf.namelist())
|
||||
|
||||
def _dir_members(subdir, suffix):
|
||||
"""data/<ns>/<subdir>/<file> or assets/<ns>/<subdir>/<file> (4 parts)."""
|
||||
out = []
|
||||
for n in names:
|
||||
parts = n.split('/')
|
||||
if len(parts) == 4 and parts[2] == subdir and n.endswith(suffix):
|
||||
out.append(n)
|
||||
return sorted(out)
|
||||
|
||||
items = _dir_members('items', '.json')
|
||||
recipes = _dir_members('recipe', '.json')
|
||||
functions = sorted(
|
||||
n for n in names
|
||||
if n.startswith('data/') and '/function/' in n and n.endswith('.mcfunction')
|
||||
)
|
||||
liquid_gains = [n for n in names if '/non_liquid_gains/' in n]
|
||||
entity_profiles = [n for n in names if '/non_entity_profiles/' in n]
|
||||
trinkets = [n for n in names if '/trinkets/' in n]
|
||||
|
||||
if not any([items, recipes, functions, liquid_gains, entity_profiles, trinkets]):
|
||||
return None
|
||||
|
||||
advancement_members = sorted(
|
||||
n for n in names
|
||||
if n.startswith('data/') and '/advancement/' in n and n.endswith('.json')
|
||||
)
|
||||
custom_recipes = [n for n in advancement_members if '/advancement/recipe/' in n]
|
||||
advancements_plain = [n for n in advancement_members if '/advancement/recipe/' not in n]
|
||||
|
||||
item_list = []
|
||||
for member in items:
|
||||
name = _item_name_from_member(member)
|
||||
ns, _, base = name.partition(':')
|
||||
tex_member = f'assets/{ns}/textures/item/{base}.png'
|
||||
item_list.append({
|
||||
'name': name,
|
||||
'texture': tex_member if tex_member in names else None,
|
||||
})
|
||||
|
||||
recipe_list = []
|
||||
for member in recipes:
|
||||
base = member.rsplit('/', 1)[-1]
|
||||
disabled = '.disabled' in base
|
||||
clean = base.replace('.json', '').replace('.disabled', '')
|
||||
try:
|
||||
data = _decode(zf.read(member))
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
data = {}
|
||||
recipe_list.append({
|
||||
'name': clean,
|
||||
'type': (data.get('type') or '').rsplit(':', 1)[-1],
|
||||
'ingredients': _ingredient_names(data),
|
||||
'result': _result_name(data),
|
||||
'disabled': disabled,
|
||||
})
|
||||
|
||||
custom_recipes_out = []
|
||||
advancements_out = []
|
||||
for member in advancement_members:
|
||||
try:
|
||||
data = _decode(zf.read(member))
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
continue
|
||||
reward = (data.get('rewards') or {}).get('function')
|
||||
name = member.rsplit('/', 1)[-1][:-5]
|
||||
if member in custom_recipes:
|
||||
custom_recipes_out.append({'name': name, 'reward': reward})
|
||||
else:
|
||||
advancements_out.append({'name': name, 'reward': reward})
|
||||
|
||||
function_list = sorted(n for n in functions if '/recipe/' not in n)
|
||||
|
||||
return {
|
||||
'items': item_list,
|
||||
'recipes': recipe_list,
|
||||
'custom_recipes': custom_recipes_out,
|
||||
'advancements': advancements_out,
|
||||
'functions': function_list,
|
||||
'custom_functions': len(functions) - len(function_list),
|
||||
'liquid_gains': liquid_gains,
|
||||
'entity_profiles': entity_profiles,
|
||||
'trinkets': trinkets,
|
||||
}
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
|
||||
def read_animation_manifest(path):
|
||||
"""Build the animation manifest for a NoN animation pack.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user