state.py 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # coding: utf-8
  2. import typing
  3. from _elementtree import Element
  4. from lxml import etree
  5. from synergine2.config import Config
  6. from synergine2.log import get_logger
  7. from opencombat.exception import StateLoadError
  8. from opencombat.exception import NotFoundError
  9. from opencombat.simulation.base import TileStrategySimulation
  10. from opencombat.simulation.subject import TileSubject
  11. from opencombat.util import get_class_from_string_path
  12. from opencombat.util import pretty_xml
  13. from opencombat.util import get_text_xml_element
  14. class State(object):
  15. def __init__(
  16. self,
  17. config: Config,
  18. state_root: Element,
  19. simulation: TileStrategySimulation,
  20. ) -> None:
  21. self._config = config
  22. self._state_root = state_root
  23. self._subjects = None # type: typing.List[TileSubject]
  24. self._simulation = simulation
  25. @property
  26. def subjects(self) -> typing.List[TileSubject]:
  27. if self._subjects is None:
  28. self._subjects = self._get_subjects()
  29. return self._subjects
  30. def _get_subjects(self) -> typing.List[TileSubject]:
  31. subjects = []
  32. subject_elements = self._state_root.find('subjects').findall('subject')
  33. for subject_element in subject_elements:
  34. subject_class_path = subject_element.find('type').text
  35. subject_class = get_class_from_string_path(
  36. self._config,
  37. subject_class_path,
  38. )
  39. subject = subject_class(self._config, self._simulation)
  40. self._fill_subject(subject, subject_element)
  41. subjects.append(subject)
  42. return subjects
  43. def _fill_subject(
  44. self,
  45. subject: TileSubject,
  46. subject_element: Element,
  47. ) -> None:
  48. subject_properties = {}
  49. subject.position = tuple(
  50. map(
  51. int,
  52. get_text_xml_element(subject_element, 'position').split(','),
  53. ),
  54. )
  55. subject.direction = float(
  56. get_text_xml_element(subject_element, 'direction'),
  57. )
  58. # TODO BS 2018-06-20: Maybe need apply this mode no ?
  59. subject.combat_mode = \
  60. get_text_xml_element(subject_element, 'combat_mode')
  61. properties_element = subject_element.find('properties')
  62. decode_properties_map = self._get_decode_properties_map()
  63. for item_element in properties_element.findall('item'):
  64. key_text = item_element.find('key').text
  65. value_text = item_element.find('value').text
  66. try:
  67. decoded_value = decode_properties_map[key_text](value_text)
  68. except KeyError:
  69. raise NotFoundError(
  70. 'You try to load property "{}" but it is unknown'.format(
  71. key_text,
  72. )
  73. )
  74. subject_properties[key_text] = decoded_value
  75. subject.properties = subject_properties
  76. def _get_decode_properties_map(self) -> typing.Dict[str, typing.Callable[[str], typing.Any]]: # nopep8
  77. return {
  78. 'SELECTION_COLOR_RGB': lambda v: tuple(map(int, v.split(','))),
  79. 'FLAG': str,
  80. 'SIDE': str,
  81. }
  82. class StateDumper(object):
  83. def __init__(
  84. self,
  85. config: Config,
  86. simulation: TileStrategySimulation,
  87. ) -> None:
  88. self._logger = get_logger('StateDumper', config)
  89. self._config = config
  90. self._simulation = simulation
  91. state_template = self._config.resolve(
  92. 'global.state_template',
  93. 'opencombat/state_template.xml',
  94. )
  95. with open(state_template, 'r') as xml_file:
  96. template_str = xml_file.read()
  97. parser = etree.XMLParser(remove_blank_text=True)
  98. self._state_root = etree.fromstring(
  99. template_str.encode('utf-8'),
  100. parser,
  101. )
  102. self._state_root_filled = False
  103. def get_state_dump(self) -> str:
  104. if not self._state_root_filled:
  105. self._fill_state_root()
  106. return pretty_xml(
  107. etree.tostring(
  108. self._state_root,
  109. ).decode('utf-8'),
  110. )
  111. def _fill_state_root(self) -> None:
  112. subjects_element = self._state_root.find('subjects')
  113. map_element = self._state_root.find('map')
  114. map_name_element = etree.SubElement(map_element, 'name')
  115. map_name_element.text = self._config.resolve('_runtime.map_dir_path')
  116. for subject in self._simulation.subjects:
  117. subject_element = etree.SubElement(subjects_element, 'subject')
  118. position_element = etree.SubElement(subject_element, 'type')
  119. position_element.text = '.'.join([
  120. subject.__module__,
  121. subject.__class__.__name__,
  122. ])
  123. position_element = etree.SubElement(subject_element, 'position')
  124. position_element.text = ','.join(map(str, subject.position))
  125. direction_element = etree.SubElement(subject_element, 'direction')
  126. direction_element.text = str(subject.direction)
  127. combat_mode_element = etree.SubElement(
  128. subject_element,
  129. 'combat_mode',
  130. )
  131. combat_mode_element.text = str(subject.combat_mode)
  132. properties_element = etree.SubElement(
  133. subject_element,
  134. 'properties',
  135. )
  136. encode_properties_map = self._get_encode_properties_map()
  137. for key, value in subject.properties.items():
  138. item_element = etree.SubElement(properties_element, 'item')
  139. key_element = etree.SubElement(item_element, 'key')
  140. value_element = etree.SubElement(item_element, 'value')
  141. key_element.text = str(key)
  142. value_element.text = encode_properties_map[key](value)
  143. self._state_root_filled = True
  144. def _get_encode_properties_map(self) -> typing.Dict[str, typing.Callable[[typing.Any], str]]: # nopep8:
  145. return {
  146. 'SELECTION_COLOR_RGB': lambda v: ','.join(map(str, v)),
  147. 'FLAG': str,
  148. 'SIDE': str,
  149. }
  150. class StateLoader(object):
  151. def __init__(
  152. self,
  153. config: Config,
  154. simulation: TileStrategySimulation,
  155. ) -> None:
  156. self._logger = get_logger('StateLoader', config)
  157. self._config = config
  158. self._simulation = simulation
  159. def get_state(
  160. self,
  161. state_file_path: str,
  162. ) -> State:
  163. return State(
  164. self._config,
  165. self._validate_and_return_state_element(state_file_path),
  166. self._simulation,
  167. )
  168. def _validate_and_return_state_element(
  169. self,
  170. state_file_path: str,
  171. ) -> Element:
  172. # open and read schema file
  173. schema_file_path = self._config.get(
  174. 'global.state_schema',
  175. 'opencombat/state.xsd',
  176. )
  177. with open(schema_file_path, 'r') as schema_file:
  178. schema_to_check = schema_file.read()
  179. # open and read xml file
  180. with open(state_file_path, 'r') as xml_file:
  181. xml_to_check = xml_file.read()
  182. xmlschema_doc = etree.fromstring(schema_to_check.encode('utf-8'))
  183. xmlschema = etree.XMLSchema(xmlschema_doc)
  184. try:
  185. doc = etree.fromstring(xml_to_check.encode('utf-8'))
  186. # check for file IO error
  187. except IOError as exc:
  188. self._logger.error(exc)
  189. raise StateLoadError('Invalid File "{}": {}'.format(
  190. state_file_path,
  191. str(exc),
  192. ))
  193. # check for XML syntax errors
  194. except etree.XMLSyntaxError as exc:
  195. self._logger.error(exc)
  196. raise StateLoadError('XML Syntax Error in "{}": {}'.format(
  197. state_file_path,
  198. str(exc.error_log),
  199. ))
  200. except Exception as exc:
  201. self._logger.error(exc)
  202. raise StateLoadError('Unknown error with "{}": {}'.format(
  203. state_file_path,
  204. str(exc),
  205. ))
  206. # validate against schema
  207. try:
  208. xmlschema.assertValid(doc)
  209. except etree.DocumentInvalid as exc:
  210. self._logger.error(exc)
  211. raise StateLoadError(
  212. 'Schema validation error with "{}": {}'.format(
  213. state_file_path,
  214. str(exc),
  215. )
  216. )
  217. except Exception as exc:
  218. self._logger.error(exc)
  219. raise StateLoadError(
  220. 'Unknown validation error with "{}": {}'.format(
  221. state_file_path,
  222. str(exc),
  223. )
  224. )
  225. return doc
  226. class StateConstructorBuilder(object):
  227. def __init__(
  228. self,
  229. config: Config,
  230. simulation: TileStrategySimulation,
  231. ) -> None:
  232. self._logger = get_logger('StateConstructorBuilder', config)
  233. self._config = config
  234. self._simulation = simulation
  235. def get_state_loader(
  236. self,
  237. ) -> StateLoader:
  238. class_address = self._config.resolve(
  239. 'global.state_loader',
  240. 'opencombat.state.StateLoader',
  241. )
  242. state_loader_class = get_class_from_string_path(
  243. self._config,
  244. class_address,
  245. )
  246. return state_loader_class(
  247. self._config,
  248. self._simulation,
  249. )
  250. def get_state_dumper(
  251. self,
  252. ) -> StateDumper:
  253. class_address = self._config.resolve(
  254. 'global.state_dumper',
  255. 'opencombat.state.StateDumper',
  256. )
  257. state_loader_class = get_class_from_string_path(
  258. self._config,
  259. class_address,
  260. )
  261. return state_loader_class(
  262. self._config,
  263. self._simulation,
  264. )