Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env python3
2"""
3Parse UE5 uncooked .uasset files to extract widget/blueprint references.
4Works outside of Unreal Editor.
5"""
6import struct
7import sys
8import json
9from pathlib import Path
10
11def parse_uasset(filepath):
12 """Parse uncooked .uasset file and extract useful references"""
13 with open(filepath, 'rb') as f:
14 data = f.read()
15
16 result = {
17 'file': str(filepath),
18 'size': len(data),
19 'game_paths': [],
20 'script_refs': [],
21 'widget_refs': [],
22 'button_bindings': [],
23 'fonts': [],
24 'textures': []
25 }
26
27 # Find /Game/ paths (widget, blueprint, asset references)
28 game_pattern = b'/Game/'
29 idx = 0
30 while True:
31 idx = data.find(game_pattern, idx)
32 if idx == -1:
33 break
34 end = idx
35 while end < len(data) and data[end] != 0 and 32 <= data[end] < 127:
36 end += 1
37 path = data[idx:end].decode('utf-8', errors='replace')
38 if path and path not in result['game_paths']:
39 result['game_paths'].append(path)
40 # Categorize
41 if 'WBP_' in path or 'Widget' in path:
42 result['widget_refs'].append(path)
43 if 'Font' in path:
44 result['fonts'].append(path)
45 if 'Texture' in path or '.uasset' in path.lower():
46 result['textures'].append(path)
47 idx += 1
48
49 # Find /Script/ references (engine classes)
50 script_pattern = b'/Script/'
51 idx = 0
52 while True:
53 idx = data.find(script_pattern, idx)
54 if idx == -1:
55 break
56 end = idx
57 while end < len(data) and data[end] != 0 and 32 <= data[end] < 127:
58 end += 1
59 path = data[idx:end].decode('utf-8', errors='replace')
60 if path and path not in result['script_refs']:
61 result['script_refs'].append(path)
62 idx += 1
63
64 # Find button event bindings (BndEvt__ pattern)
65 evt_pattern = b'BndEvt__'
66 idx = 0
67 while True:
68 idx = data.find(evt_pattern, idx)
69 if idx == -1:
70 break
71 end = idx
72 while end < len(data) and data[end] != 0 and 32 <= data[end] < 127:
73 end += 1
74 binding = data[idx:end].decode('utf-8', errors='replace')
75 if binding and binding not in result['button_bindings']:
76 result['button_bindings'].append(binding)
77 idx += 1
78
79 return result
80
81def compare_widgets(file1, file2):
82 """Compare two widget assets"""
83 r1 = parse_uasset(file1)
84 r2 = parse_uasset(file2)
85
86 comparison = {
87 'file1': str(file1),
88 'file2': str(file2),
89 'widget_refs': {
90 'only_in_file1': [x for x in r1['widget_refs'] if x not in r2['widget_refs']],
91 'only_in_file2': [x for x in r2['widget_refs'] if x not in r1['widget_refs']],
92 'common': [x for x in r1['widget_refs'] if x in r2['widget_refs']]
93 },
94 'button_bindings': {
95 'only_in_file1': [x for x in r1['button_bindings'] if x not in r2['button_bindings']],
96 'only_in_file2': [x for x in r2['button_bindings'] if x not in r1['button_bindings']],
97 'common': [x for x in r1['button_bindings'] if x in r2['button_bindings']]
98 },
99 'game_paths': {
100 'only_in_file1': [x for x in r1['game_paths'] if x not in r2['game_paths']],
101 'only_in_file2': [x for x in r2['game_paths'] if x not in r1['game_paths']]
102 }
103 }
104 return comparison
105
106if __name__ == '__main__':
107 if len(sys.argv) < 2:
108 print("Usage:")
109 print(" python parse_uasset.py <file.uasset> # Parse single file")
110 print(" python parse_uasset.py <file1> <file2> # Compare two files")
111 sys.exit(1)
112
113 if len(sys.argv) == 2:
114 result = parse_uasset(sys.argv[1])
115 print(json.dumps(result, indent=2))
116 else:
117 comparison = compare_widgets(sys.argv[1], sys.argv[2])
118 print(json.dumps(comparison, indent=2))