dolphin.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import multiprocessing
  2. import logging
  3. import os
  4. from collections import Counter
  5. from flipper.utils.fff import FlipperFormatFile
  6. from flipper.utils.templite import Templite
  7. from .icon import ImageTools, file2image
  8. def _convert_image_to_bm(pair: set):
  9. source_filename, destination_filename = pair
  10. image = file2image(source_filename)
  11. image.write(destination_filename)
  12. def _convert_image(source_filename: str):
  13. image = file2image(source_filename)
  14. return image.data
  15. class DolphinBubbleAnimation:
  16. FILE_TYPE = "Flipper Animation"
  17. FILE_VERSION = 1
  18. def __init__(
  19. self,
  20. name: str,
  21. min_butthurt: int,
  22. max_butthurt: int,
  23. min_level: int,
  24. max_level: int,
  25. weight: int,
  26. ):
  27. # Manifest
  28. self.name = name
  29. self.min_butthurt = min_butthurt
  30. self.max_butthurt = max_butthurt
  31. self.min_level = min_level
  32. self.max_level = max_level
  33. self.weight = weight
  34. # Meta and data
  35. self.meta = {}
  36. self.frames = []
  37. self.bubbles = []
  38. self.bubble_slots = None
  39. # Logging
  40. self.logger = logging.getLogger("DolphinBubbleAnimation")
  41. def load(self, animation_directory: str):
  42. if not os.path.isdir(animation_directory):
  43. raise Exception(f"Animation folder doesn't exists: { animation_directory }")
  44. meta_filename = os.path.join(animation_directory, "meta.txt")
  45. if not os.path.isfile(meta_filename):
  46. raise Exception(f"Animation meta file doesn't exists: { meta_filename }")
  47. self.logger.info(f"Loading meta from {meta_filename}")
  48. file = FlipperFormatFile()
  49. file.load(meta_filename)
  50. # Check file header
  51. filetype, version = file.getHeader()
  52. assert filetype == self.FILE_TYPE
  53. assert version == self.FILE_VERSION
  54. max_frame_number = None
  55. unique_frames = None
  56. total_frames_count = None
  57. try:
  58. # Main meta
  59. self.meta["Width"] = file.readKeyInt("Width")
  60. self.meta["Height"] = file.readKeyInt("Height")
  61. self.meta["Passive frames"] = file.readKeyInt("Passive frames")
  62. self.meta["Active frames"] = file.readKeyInt("Active frames")
  63. self.meta["Frames order"] = file.readKeyIntArray("Frames order")
  64. self.meta["Active cycles"] = file.readKeyInt("Active cycles")
  65. self.meta["Frame rate"] = file.readKeyInt("Frame rate")
  66. self.meta["Duration"] = file.readKeyInt("Duration")
  67. self.meta["Active cooldown"] = file.readKeyInt("Active cooldown")
  68. self.bubble_slots = file.readKeyInt("Bubble slots")
  69. # Sanity Check
  70. assert self.meta["Width"] > 0 and self.meta["Width"] <= 128
  71. assert self.meta["Height"] > 0 and self.meta["Height"] <= 128
  72. assert self.meta["Passive frames"] > 0
  73. assert self.meta["Active frames"] >= 0
  74. assert self.meta["Frames order"]
  75. if self.meta["Active frames"] > 0:
  76. assert self.meta["Active cooldown"] > 0
  77. assert self.meta["Active cycles"] > 0
  78. else:
  79. assert self.meta["Active cooldown"] == 0
  80. assert self.meta["Active cycles"] == 0
  81. assert self.meta["Frame rate"] > 0
  82. assert self.meta["Duration"] >= 0
  83. # Frames sanity check
  84. max_frame_number = max(self.meta["Frames order"])
  85. ordered_frames_count = len(self.meta["Frames order"])
  86. for i in range(max_frame_number + 1):
  87. frame_filename = os.path.join(animation_directory, f"frame_{i}.png")
  88. assert os.path.isfile(frame_filename)
  89. self.frames.append(frame_filename)
  90. # Sanity check
  91. unique_frames = set(self.meta["Frames order"])
  92. unique_frames_count = len(unique_frames)
  93. if unique_frames_count != max_frame_number + 1:
  94. self.logger.warning(f"Not all frames were used in {self.name}")
  95. total_frames_count = self.meta["Passive frames"] + (
  96. self.meta["Active frames"] * self.meta["Active cycles"]
  97. )
  98. # Extra checks
  99. assert self.meta["Passive frames"] <= total_frames_count
  100. assert self.meta["Active frames"] <= total_frames_count
  101. assert (
  102. self.meta["Passive frames"] + self.meta["Active frames"]
  103. == ordered_frames_count
  104. )
  105. except EOFError:
  106. raise Exception("Invalid meta file: too short")
  107. except AssertionError as e:
  108. self.logger.exception(e)
  109. self.logger.error(f"Animation {self.name} got incorrect meta")
  110. raise Exception("Meta file is invalid: incorrect data")
  111. # Bubbles
  112. while True:
  113. try:
  114. # Bubble data
  115. bubble = {}
  116. bubble["Slot"] = file.readKeyInt("Slot")
  117. bubble["X"] = file.readKeyInt("X")
  118. bubble["Y"] = file.readKeyInt("Y")
  119. bubble["Text"] = file.readKey("Text")
  120. bubble["AlignH"] = file.readKey("AlignH")
  121. bubble["AlignV"] = file.readKey("AlignV")
  122. bubble["StartFrame"] = file.readKeyInt("StartFrame")
  123. bubble["EndFrame"] = file.readKeyInt("EndFrame")
  124. # Sanity check
  125. assert bubble["Slot"] <= self.bubble_slots
  126. assert bubble["X"] >= 0 and bubble["X"] < 128
  127. assert bubble["Y"] >= 0 and bubble["Y"] < 128
  128. assert len(bubble["Text"]) > 0
  129. assert bubble["AlignH"] in ["Left", "Center", "Right"]
  130. assert bubble["AlignV"] in ["Bottom", "Center", "Top"]
  131. assert bubble["StartFrame"] < total_frames_count
  132. assert bubble["EndFrame"] < total_frames_count
  133. assert bubble["EndFrame"] >= bubble["StartFrame"]
  134. # Store bubble
  135. self.bubbles.append(bubble)
  136. except AssertionError as e:
  137. self.logger.exception(e)
  138. self.logger.error(
  139. f"Animation {self.name} bubble slot {bubble['Slot']} got incorrect data: {bubble}"
  140. )
  141. raise Exception("Meta file is invalid: incorrect bubble data")
  142. except EOFError:
  143. break
  144. def prepare(self):
  145. bubbles_in_slots = Counter([bubble["Slot"] for bubble in self.bubbles])
  146. last_slot = -1
  147. bubble_index = 0
  148. for bubble in self.bubbles:
  149. slot = bubble["Slot"]
  150. if slot == last_slot:
  151. bubble_index += 1
  152. else:
  153. last_slot = slot
  154. bubble_index = 0
  155. bubble["_BubbleIndex"] = bubble_index
  156. bubbles_in_slots[slot] -= 1
  157. if bubbles_in_slots[slot] != 0:
  158. bubble["_NextBubbleIndex"] = bubble_index + 1
  159. def save(self, output_directory: str):
  160. animation_directory = os.path.join(output_directory, self.name)
  161. os.makedirs(animation_directory, exist_ok=True)
  162. meta_filename = os.path.join(animation_directory, "meta.txt")
  163. file = FlipperFormatFile()
  164. file.setHeader(self.FILE_TYPE, self.FILE_VERSION)
  165. file.writeEmptyLine()
  166. # Write meta data
  167. file.writeKey("Width", self.meta["Width"])
  168. file.writeKey("Height", self.meta["Height"])
  169. file.writeKey("Passive frames", self.meta["Passive frames"])
  170. file.writeKey("Active frames", self.meta["Active frames"])
  171. file.writeKey("Frames order", self.meta["Frames order"])
  172. file.writeKey("Active cycles", self.meta["Active cycles"])
  173. file.writeKey("Frame rate", self.meta["Frame rate"])
  174. file.writeKey("Duration", self.meta["Duration"])
  175. file.writeKey("Active cooldown", self.meta["Active cooldown"])
  176. file.writeEmptyLine()
  177. file.writeKey("Bubble slots", self.bubble_slots)
  178. file.writeEmptyLine()
  179. # Write bubble data
  180. for bubble in self.bubbles:
  181. file.writeKey("Slot", bubble["Slot"])
  182. file.writeKey("X", bubble["X"])
  183. file.writeKey("Y", bubble["Y"])
  184. file.writeKey("Text", bubble["Text"])
  185. file.writeKey("AlignH", bubble["AlignH"])
  186. file.writeKey("AlignV", bubble["AlignV"])
  187. file.writeKey("StartFrame", bubble["StartFrame"])
  188. file.writeKey("EndFrame", bubble["EndFrame"])
  189. file.writeEmptyLine()
  190. file.save(meta_filename)
  191. to_pack = []
  192. for index, frame in enumerate(self.frames):
  193. to_pack.append(
  194. (frame, os.path.join(animation_directory, f"frame_{index}.bm"))
  195. )
  196. if ImageTools.is_processing_slow():
  197. pool = multiprocessing.Pool()
  198. pool.map(_convert_image_to_bm, to_pack)
  199. else:
  200. for image in to_pack:
  201. _convert_image_to_bm(image)
  202. def process(self):
  203. if ImageTools.is_processing_slow():
  204. pool = multiprocessing.Pool()
  205. self.frames = pool.map(_convert_image, self.frames)
  206. else:
  207. self.frames = list(_convert_image(frame) for frame in self.frames)
  208. class DolphinManifest:
  209. FILE_TYPE = "Flipper Animation Manifest"
  210. FILE_VERSION = 1
  211. TEMPLATE_DIRECTORY = os.path.join(
  212. os.path.dirname(os.path.realpath(__file__)), "templates"
  213. )
  214. TEMPLATE_H = os.path.join(TEMPLATE_DIRECTORY, "dolphin.h.tmpl")
  215. TEMPLATE_C = os.path.join(TEMPLATE_DIRECTORY, "dolphin.c.tmpl")
  216. def __init__(self):
  217. self.animations = []
  218. self.logger = logging.getLogger("DolphinManifest")
  219. def load(self, source_directory: str):
  220. manifest_filename = os.path.join(source_directory, "manifest.txt")
  221. file = FlipperFormatFile()
  222. file.load(manifest_filename)
  223. # Check file header
  224. filetype, version = file.getHeader()
  225. assert filetype == self.FILE_TYPE
  226. assert version == self.FILE_VERSION
  227. # Load animation data
  228. while True:
  229. try:
  230. # Read animation spcification
  231. name = file.readKey("Name")
  232. min_butthurt = file.readKeyInt("Min butthurt")
  233. max_butthurt = file.readKeyInt("Max butthurt")
  234. min_level = file.readKeyInt("Min level")
  235. max_level = file.readKeyInt("Max level")
  236. weight = file.readKeyInt("Weight")
  237. assert len(name) > 0
  238. assert min_butthurt >= 0
  239. assert max_butthurt >= 0 and max_butthurt >= min_butthurt
  240. assert min_level >= 0
  241. assert max_level >= 0 and max_level >= min_level
  242. assert weight >= 0
  243. # Initialize animation
  244. animation = DolphinBubbleAnimation(
  245. name, min_butthurt, max_butthurt, min_level, max_level, weight
  246. )
  247. # Load Animation meta and frames
  248. animation.load(os.path.join(source_directory, name))
  249. # Add to array
  250. self.animations.append(animation)
  251. except EOFError:
  252. break
  253. def _renderTemplate(self, template_filename: str, output_filename: str, **kwargs):
  254. template = Templite(filename=template_filename)
  255. output = template.render(**kwargs)
  256. with open(output_filename, "w", newline="\n") as file:
  257. file.write(output)
  258. def save2code(self, output_directory: str, symbol_name: str):
  259. # Process frames
  260. for animation in self.animations:
  261. animation.process()
  262. # Prepare substitution data
  263. for animation in self.animations:
  264. animation.prepare()
  265. # Render Header
  266. self._renderTemplate(
  267. self.TEMPLATE_H,
  268. os.path.join(output_directory, f"assets_{symbol_name}.h"),
  269. animations=self.animations,
  270. symbol_name=symbol_name,
  271. )
  272. # Render Source
  273. self._renderTemplate(
  274. self.TEMPLATE_C,
  275. os.path.join(output_directory, f"assets_{symbol_name}.c"),
  276. animations=self.animations,
  277. symbol_name=symbol_name,
  278. )
  279. def save2folder(self, output_directory: str):
  280. manifest_filename = os.path.join(output_directory, "manifest.txt")
  281. file = FlipperFormatFile()
  282. file.setHeader(self.FILE_TYPE, self.FILE_VERSION)
  283. file.writeEmptyLine()
  284. for animation in self.animations:
  285. file.writeKey("Name", animation.name)
  286. file.writeKey("Min butthurt", animation.min_butthurt)
  287. file.writeKey("Max butthurt", animation.max_butthurt)
  288. file.writeKey("Min level", animation.min_level)
  289. file.writeKey("Max level", animation.max_level)
  290. file.writeKey("Weight", animation.weight)
  291. file.writeEmptyLine()
  292. animation.save(output_directory)
  293. file.save(manifest_filename)
  294. def save(self, output_directory: str, symbol_name: str):
  295. os.makedirs(output_directory, exist_ok=True)
  296. if symbol_name:
  297. self.save2code(output_directory, symbol_name)
  298. else:
  299. self.save2folder(output_directory)
  300. class Dolphin:
  301. def __init__(self):
  302. self.manifest = DolphinManifest()
  303. self.logger = logging.getLogger("Dolphin")
  304. def load(self, source_directory: str):
  305. assert os.path.isdir(source_directory)
  306. # Load Manifest
  307. self.logger.info(f"Loading directory {source_directory}")
  308. self.manifest.load(source_directory)
  309. def pack(self, output_directory: str, symbol_name: str = None):
  310. self.manifest.save(output_directory, symbol_name)