-
Notifications
You must be signed in to change notification settings - Fork 1
/
macros.py
272 lines (209 loc) · 9.35 KB
/
macros.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
"""Loading and processing of game macro and component files."""
import copy
import re
import logging
from lxml import etree
from misc import get_path_in_ext
LOG = logging.getLogger(__name__)
class Macro:
"""Class that describes a macro.
Members:
name: in-game name of the macro.
type: macro type, a.k.a. class. E.g. engine, shieldgenerator, ship_xl.
connections: list of (connection_id, macro_id) of connected components.
properties: a dictionary containing parsed macro data.
"""
__slots__ = ('name', 'type', 'connections', 'properties')
def __init__(self, name, macro_type, properties):
self.name = name
self.type = macro_type
self.connections = []
self.properties = properties
def add_connection(self, conn_id, macro_id):
"""Add a connection to this macro.
Used by MacroDB.
"""
self.connections.append((conn_id, macro_id))
def noop_parser(_name, _entity_type, _node):
"""Macro and component parser that does nothing and returns an empty dict.
Used as default parsers for Macro.
"""
return {}
class MacroDB:
"""Database that takes care of loading macros and components and resolving
dependencies.
Macros and components are processed via custom functions passed through
set_macro_parser and set_component_parser.
Members:
floader: the file loader used to resolve dependencies.
macros: dict of Macro objects keyed by the macro id.
macros_by_type: dict of macro_type -> [macro_name].
dependencies: unresolved dependencies.
macro_path_resolver: function that resolves a macro id to a game .xml file
path.
component_path_resolver: function that resolve a component name to a game
.xml file path.
macro_parser: function that receives the macro name, macro type (class) and
macro <properties> node and returns a dictionary that will be
saved as macro's properties.
component_parser: function that receives the component name, component type
and component XML node and returns a dictionary that will
be combined into the one returned by macro_parser.
"""
def __init__(self, floader):
"""Initialize the macro database.
Arguments:
floader: file loader that will be used by this object.
"""
self.macro_index = {}
self.component_index = {}
self.floader = None
self.macros = {}
self.macros_by_type = {}
self.dependencies = set()
self.macro_parser = noop_parser
self.component_parser = noop_parser
self.set_floader(floader)
def _load_index(self, path, dest):
"""Load an index file.
Arguments:
path: game path to the index file.
dest: dictionary into which to insert index entries.
"""
with self.floader.open_file(path) as idx_file:
idx_tree = etree.parse(idx_file)
for entry in idx_tree.xpath('./entry[@name][@value]'):
dest[entry.get('name')] = entry.get('value').replace('\\', '/') + '.xml'
def _fix_missing_index_entries(self):
"""Fix indexes. Add missing entries or repair broken ones."""
self.component_index['cockpit_invisible_escapepod'] = \
'assets/units/size_s/cockpit_invisible_escapepod.xml'
def set_floader(self, floader):
"""Set the file loader and reset the index.
Arguments:
floader: file loader that will be used by this object.
"""
self.floader = floader
self.macro_index = {}
self.component_index = {}
for ext_name in [None] + floader.get_extensions():
macros_path = get_path_in_ext('index/macros.xml', ext_name)
components_path = get_path_in_ext('index/components.xml', ext_name)
self._load_index(macros_path, self.macro_index)
self._load_index(components_path, self.component_index)
self._fix_missing_index_entries()
def set_macro_parser(self, macro_parser):
"""Sets the macro parser."""
self.macro_parser = macro_parser
def set_component_parser(self, component_parser):
"""Sets the component parser."""
self.component_parser = component_parser
def load_component_properties(self, comp_name):
"""Loads a component, parses it and returns the properties dict.
Arguments:
comp_name: name (id) of component to load.
"""
path = self.component_index.get(comp_name)
if not path:
LOG.error('Failed to load component %s, not found in index',
comp_name)
return {}
with self.floader.open_file(path) as comp_file:
comp_tree = etree.parse(comp_file)
comp_xpath = "./component[@name='{}']".format(comp_name)
comp_nodes = comp_tree.xpath(comp_xpath)
if len(comp_nodes) > 1:
LOG.error('Failed to load component properties from %s: '
'too many <properties> nodes', path)
elif comp_nodes:
comp_node = comp_nodes[0]
# some pesky component has a space in its class
comp_type = comp_node.get('class').strip()
return self.component_parser(comp_name, comp_type, comp_node)
else:
LOG.warning('No components with name %s in file %s',
comp_name, path)
return {}
def load_macro_xml_file(self, path):
"""Loads macros from a game .xml file.
Arguments:
path: path to game .xml file.
E.g.: assets/props/Engine/macros/engine_(...)_macro.xml
"""
with self.floader.open_file(path) as macro_file:
tree = etree.parse(macro_file)
found_macro = False
for macro_node in tree.xpath('./macro[@name][@class]'):
found_macro = True
macro_name = macro_node.get('name')
macro_type = macro_node.get('class')
properties = {}
prop_nodes = macro_node.xpath('./properties')
if len(prop_nodes) > 1:
LOG.error('Failed to load macro properties from %s: too '
'many <properties> nodes', path)
elif prop_nodes:
# parse properties
properties = \
self.macro_parser(macro_name, macro_type, prop_nodes[0])
comp_nodes = macro_node.xpath('./component')
if len(comp_nodes) > 1:
LOG.error('Failed to load component properties from %s: '
'too many <properties> nodes', path)
elif comp_nodes:
comp_name = comp_nodes[0].get('ref')
# parse properties from the component
comp_props = self.load_component_properties(comp_name)
properties.update(comp_props)
macro = Macro(macro_name, macro_type, properties)
connections_xpath = './connections/connection[@ref]'
for conn_node in macro_node.xpath(connections_xpath):
conn_ref = conn_node.get('ref')
for conn_m_node in conn_node.xpath('./macro[@ref]'):
macro_ref = conn_m_node.get('ref')
if macro_ref not in self.macros:
self.dependencies.add(macro_ref)
macro.add_connection(conn_ref, macro_ref)
# save macro, remove dependency if it exists
self.macros[macro_name] = macro
self.dependencies.discard(macro_name)
t_macros = self.macros_by_type.setdefault(macro_type, [])
t_macros.append(macro_name)
if not found_macro:
LOG.warning('No macros found in file %s', path)
def _resolve_step(self):
"""One step in the dependency resolution algorithm.
Returns true if the set of dependencies has changed.
"""
# step 1: remove deps that are satisfied just to be sure
for ref in list(self.dependencies):
if ref in self.macros:
self.dependencies.remove(ref)
# step 2: make a copy of the dependency set
deps_before = copy.deepcopy(self.dependencies)
# step 3: try to load dependencies
for ref in deps_before:
path = self.macro_index.get(ref)
if not path:
LOG.error('Failed to load ref %s, not found in index', ref)
continue
if not self.floader.file_exists(path):
LOG.error('Failed to load ref %s, file %s not found', ref, path)
continue
self.load_macro_xml_file(path)
# step 4: return True if the new dependency set is different
return deps_before != self.dependencies
def resolve_dependencies(self):
"""Loads macros that aren't loaded yet but that are referred to by
loaded macros.
Returns True if all dependencies were resolved.
"""
# while there are dependencies resolve them
# when the set of dependencies doesn't change anymore stop
while self.dependencies:
if not self._resolve_step():
LOG.error('Failed to resolve all dependencies. Remaining: %s',
self.dependencies)
break
# return True if no dependencies left
return not self.dependencies