dolphin.py 14 KB

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