RomAssetEditor.gd 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. class_name AssetEditor
  2. extends AssetRipper
  3. const CUR_SPRITE_TEXT: String = "Current Sprite:\n%s"
  4. var list_index: int = 0
  5. var sprite_list: Array
  6. var cached_sprites: Dictionary[String, Dictionary]
  7. var tile_atlas: Texture
  8. var rom_provided: bool
  9. var source_path: String
  10. var tile_index: int = 0
  11. var columns: int = 4
  12. var sheet_size := Vector2i(16, 16)
  13. var palette_base: String = "Tile"
  14. var palettes: Dictionary
  15. var tiles: Dictionary
  16. @onready var rom_required: Label = %RomRequired
  17. @onready var file_dialog: FileDialog = %FileDialog
  18. @onready var image_preview: TextureRect = %ImagePreview
  19. @onready var green_preview: TextureRect = %GreenPreview
  20. @onready var tiles_preview: TextureRect = %TilesPreview
  21. @onready var cur_sprite: Label = %CurSprite
  22. @onready var preview: Sprite2D = %Preview
  23. @onready var buttons: HBoxContainer = %Buttons
  24. @onready var palette_override: LineEdit = %PaletteOverride
  25. @onready var scroll_container: ScrollContainer = %ScrollContainer
  26. @onready var json_container: VBoxContainer = %JSONContainer
  27. @onready var json_edit: TextEdit = %JSONEdit
  28. func _ready() -> void:
  29. Global.get_node("GameHUD").hide()
  30. if Global.rom_path != "":
  31. on_file_selected(Global.rom_path)
  32. func _exit_tree() -> void:
  33. Global.get_node("GameHUD").show()
  34. func _unhandled_input(event: InputEvent) -> void:
  35. if not rom_provided:
  36. return
  37. if event is InputEventMouseMotion:
  38. var is_snapped = not Input.is_action_pressed("editor_cam_fast")
  39. var mouse_pos: Vector2 = event.position + Vector2(
  40. scroll_container.scroll_horizontal,
  41. scroll_container.scroll_vertical
  42. ) + Vector2(-4, -8)
  43. preview.position = Vector2i(mouse_pos).snappedi(8 if is_snapped else 1)
  44. preview.visible = true
  45. if preview.position.x + 8 > sheet_size.x:
  46. preview.visible = false
  47. if preview.position.y + 8 > sheet_size.y:
  48. preview.visible = false
  49. if not preview.is_visible_in_tree(): return
  50. var direction: int = 0
  51. if event.is_action_pressed("editor_cam_left"): direction = -1
  52. if event.is_action_pressed("editor_cam_right"): direction = 1
  53. if event.is_action_pressed("pick_tile"):
  54. if tiles.has(preview.position):
  55. tile_index = tiles[preview.position].index
  56. preview.flip_h = tiles[preview.position].flip_h
  57. preview.flip_v = tiles[preview.position].flip_v
  58. if Input.is_action_pressed("editor_select"):
  59. if tiles[preview.position].has("palette"):
  60. palette_override.text = tiles[preview.position]["palette"]
  61. else:
  62. palette_override.text = ""
  63. preview.region_rect = Rect2i(index_to_coords(tile_index, 32) * 8, Vector2i(8, 8))
  64. if direction != 0:
  65. var multiply = 1 if not Input.is_action_pressed("editor_cam_fast") else 8
  66. tile_index = wrapi(tile_index + direction * multiply, 0, 512)
  67. preview.region_rect = Rect2i(index_to_coords(tile_index, 32) * 8, Vector2i(8, 8))
  68. if event.is_action_pressed("jump_0"): preview.flip_h = not preview.flip_h
  69. if event.is_action_pressed("run_0"): preview.flip_v = not preview.flip_v
  70. var left_click: bool = event.is_action_pressed("mb_left")
  71. var right_click: bool = event.is_action_pressed("mb_right")
  72. if left_click or right_click:
  73. if left_click:
  74. tiles[preview.position] = {
  75. "index": tile_index,
  76. "flip_h": preview.flip_h,
  77. "flip_v": preview.flip_v
  78. }
  79. if not palette_override.text.is_empty():
  80. tiles[preview.position]["palette"] = palette_override.text
  81. else:
  82. tiles.erase(preview.position)
  83. update_edited_image()
  84. func _process(_delta: float) -> void:
  85. if Input.is_action_pressed("editor_select") == false:
  86. return
  87. var left_click: bool = Input.is_action_pressed("mb_left")
  88. var right_click: bool = Input.is_action_pressed("mb_right")
  89. if left_click or right_click:
  90. if left_click:
  91. tiles[preview.position] = {
  92. "index": tile_index,
  93. "flip_h": preview.flip_h,
  94. "flip_v": preview.flip_v
  95. }
  96. if not palette_override.text.is_empty():
  97. tiles[preview.position]["palette"] = palette_override.text
  98. else:
  99. tiles.erase(preview.position)
  100. update_edited_image()
  101. func update_edited_image() -> void:
  102. var tiles_images: Array[Image] = get_tile_images(image_preview.texture.get_image().get_size(), tiles, palettes)
  103. tiles_preview.texture = ImageTexture.create_from_image(tiles_images[0])
  104. green_preview.texture = ImageTexture.create_from_image(tiles_images[1])
  105. func get_tile_images(
  106. img_size: Vector2i, tile_list: Dictionary, palette_lists: Dictionary
  107. ) -> Array[Image]:
  108. var image := Image.create(img_size.x, img_size.y, false, Image.FORMAT_RGBA8)
  109. var green_image := Image.create(img_size.x, img_size.y, false, Image.FORMAT_RGBA8)
  110. for palette_name in palette_lists.keys():
  111. var cur_column: int = 0
  112. var offset := Vector2.ZERO
  113. var pal_json: String = FileAccess.get_file_as_string(
  114. PALETTES_FOLDER % [DEFAULT_PALETTE_GROUP, palette_name])
  115. var pal_dict: Dictionary = JSON.parse_string(pal_json).palettes
  116. for palette_id: String in palette_lists[palette_name]:
  117. var palette: Array = pal_dict.get(palette_id, PREVIEW_PALETTE)
  118. for tile_pos: Vector2 in tile_list:
  119. var tile_dict: Dictionary = tile_list[tile_pos]
  120. var tile_palette: String = tile_dict.get("palette", palette_base)
  121. if tile_palette == palette_name:
  122. var destination: Vector2 = tile_pos + offset
  123. if destination.x < img_size.x and destination.y < img_size.y:
  124. draw_green(green_image, destination)
  125. draw_tile(
  126. false,
  127. image,
  128. tile_dict.get("index", 0),
  129. destination,
  130. palette,
  131. tile_dict.get("flip_h", false),
  132. tile_dict.get("flip_v", false)
  133. )
  134. cur_column += 1
  135. if cur_column >= columns:
  136. cur_column = 0
  137. offset.x = 0
  138. offset.y += sheet_size.y
  139. else:
  140. offset.x += sheet_size.x
  141. return [image, green_image]
  142. func on_file_selected(path: String) -> void:
  143. rom = FileAccess.get_file_as_bytes(path)
  144. prg_rom_size = rom[4] * 16384
  145. chr_rom = rom.slice(16 + prg_rom_size)
  146. rom_provided = true
  147. rom_required.hide()
  148. buttons.show()
  149. # setup sprite atlas for placing tiles
  150. var atlas := Image.create(256, 256, false, Image.FORMAT_RGBA8)
  151. for index in range(512):
  152. var pos: Vector2i = index_to_coords(index, 32) * 8
  153. draw_tile(false, atlas, index, pos, PREVIEW_PALETTE)
  154. preview.texture = ImageTexture.create_from_image(atlas)
  155. #
  156. var list_json: String = FileAccess.get_file_as_string(SPRITE_LIST_PATH)
  157. var list_dict: Dictionary = JSON.parse_string(list_json)
  158. sprite_list = list_dict.get("sprites", [])
  159. cycle_list(0)
  160. func load_sprite(sprite_dict: Dictionary) -> void:
  161. source_path = sprite_dict.source_path
  162. if source_path.begins_with("res://"):
  163. image_preview.texture = load(source_path)
  164. else:
  165. var image := Image.load_from_file(source_path)
  166. image_preview.texture = ImageTexture.create_from_image(image)
  167. cur_sprite.text = CUR_SPRITE_TEXT % source_path.get_file()
  168. columns = str_to_var(sprite_dict.get("columns", "4"))
  169. sheet_size = str_to_var(sprite_dict.get("sheet_size", "Vector2i(16, 16)"))
  170. palette_base = sprite_dict.get("palette_base", "Tile")
  171. var palettes_var: Variant = str_to_var(sprite_dict.get("palettes", var_to_str(get_default_palettes())))
  172. if typeof(palettes_var) == TYPE_ARRAY:
  173. palettes = {}
  174. palettes[palette_base] = palettes_var
  175. elif typeof(palettes_var) == TYPE_DICTIONARY:
  176. palettes = palettes_var
  177. tiles = str_to_var(sprite_dict.get("tiles", "{}"))
  178. update_palettes()
  179. update_edited_image()
  180. func save_sprite() -> void:
  181. var sprite_dict: Dictionary = get_as_dict()
  182. var destination_path: String = png_path_to_json(sprite_dict.source_path)
  183. var json_string: String = JSON.stringify(sprite_dict)
  184. DirAccess.make_dir_recursive_absolute(destination_path.get_base_dir())
  185. var file: FileAccess = FileAccess.open(destination_path, FileAccess.WRITE)
  186. file.store_line(json_string)
  187. file.close()
  188. # save green over original image
  189. var base_image: Image = image_preview.texture.get_image()
  190. var green_image: Image = green_preview.texture.get_image()
  191. for y in range(green_image.get_size().y):
  192. for x in range(green_image.get_size().x):
  193. var found_color: Color = green_image.get_pixel(x, y)
  194. if found_color.a > 0:
  195. base_image.set_pixel(x, y, found_color)
  196. base_image.save_png(sprite_dict.source_path)
  197. func cycle_list(add_index: int) -> void:
  198. if add_index != 0:
  199. cached_sprites[sprite_list[list_index]] = get_as_dict()
  200. list_index = wrapi(list_index + add_index, 0, sprite_list.size())
  201. if sprite_list[list_index] in cached_sprites:
  202. load_sprite(cached_sprites[sprite_list[list_index]])
  203. else:
  204. var json_path: String = sprite_list[list_index].replace(
  205. "res://Assets/Sprites/", "res://Resources/AssetRipper/Sprites/"
  206. ).replace(".png", ".json")
  207. if FileAccess.file_exists(json_path):
  208. var json_string: String = FileAccess.get_file_as_string(json_path)
  209. load_sprite(JSON.parse_string(json_string))
  210. else:
  211. load_sprite({"source_path": sprite_list[list_index]})
  212. func get_as_dict() -> Dictionary:
  213. return {
  214. "source_path": source_path,
  215. "columns": var_to_str(columns),
  216. "sheet_size": var_to_str(sheet_size),
  217. "palette_base": palette_base,
  218. "palettes": var_to_str(palettes),
  219. "tiles": var_to_str(tiles)
  220. }
  221. func get_default_palettes() -> Dictionary:
  222. var pal_json: String = FileAccess.get_file_as_string(
  223. PALETTES_FOLDER % [DEFAULT_PALETTE_GROUP, palette_base])
  224. var pal_dict: Dictionary = JSON.parse_string(pal_json)
  225. var default_palettes: Array = pal_dict.get("palettes", []).keys()
  226. return {palette_base: default_palettes}
  227. func update_palettes(load_textedit: bool = false) -> void:
  228. if load_textedit:
  229. var dict: Dictionary = JSON.parse_string(json_edit.text)
  230. columns = dict.get("columns", 1)
  231. var size_array: Array = dict.get("sheet_size", [16, 16])
  232. sheet_size = Vector2i(size_array[0], size_array[1])
  233. palette_base = dict.get("palette_base", "Tile")
  234. var pal_var: Variant = dict.get("palettes", get_default_palettes())
  235. if typeof(pal_var) == TYPE_ARRAY:
  236. palettes = {}
  237. palettes[palette_base] = pal_var
  238. elif typeof(pal_var) == TYPE_DICTIONARY:
  239. palettes = pal_var
  240. update_edited_image()
  241. json_edit.text = JSON.stringify(
  242. {
  243. "columns": columns,
  244. "sheet_size": [sheet_size.x, sheet_size.y],
  245. "palette_base": palette_base,
  246. "palettes": palettes
  247. }, "\t", false)
  248. func toggle_palettes_view(toggled_on: bool) -> void:
  249. json_container.visible = toggled_on
  250. scroll_container.visible = not toggled_on
  251. func draw_green(
  252. image: Image,
  253. pos: Vector2i
  254. ) -> void:
  255. for y in range(8):
  256. for x in range(8):
  257. image.set_pixelv(Vector2i(x, y) + pos, Color.GREEN)