Skip to content

Instantly share code, notes, and snippets.

@NoodleSushi
Last active December 15, 2020 01:29
Show Gist options
  • Save NoodleSushi/3d5c1fceb049b64340f5b94bbfc67fa6 to your computer and use it in GitHub Desktop.
Save NoodleSushi/3d5c1fceb049b64340f5b94bbfc67fa6 to your computer and use it in GitHub Desktop.
import io
import re
import os as OS
import json as Json
import pyperclip
import math
from rectpack import newPacker
from rectpack.packer import Packer
from PIL import Image
from types import FunctionType
from types import LambdaType
from typing import Union
from typing import List
MODULE_COMMENT = r"""
Source code generated from:
_ ___ _ _ _
_ __ (_) __ _ _ ___ / __|| |_ _ _ __| |(_) ___
| ' \ | |/ _|| '_|/ _ \\__ \| _|| || |/ _` || |/ _ \
|_|_|_||_|\__||_| \___/|___/ \__| \_,_|\__,_||_|\___/
__ __ _ __ _ _ ___
| \/ | __ _ | |__ ___ / _|(_)| | ___ __ __|_ )
| |\/| |/ _` || / // -_)| _|| || |/ -_) \ V / / /
|_| |_|\__,_||_\_\\___||_| |_||_|\___| \_/ /___|
by NoodleSushi
https://gist.github.com/NoodleSushi/3d5c1fceb049b64340f5b94bbfc67fa6
""".replace("\n", "\n// ")
class DIR():
OUTPUT: str = "_OUTPUT"
VSCODE: str = ".vscode"
class FILE():
CONFIG: str = OS.path.join(DIR.OUTPUT, "config.json")
MAIN: str = "main.lua"
SOURCE: str = OS.path.join(DIR.OUTPUT, "source.ms")
class ConfigHandler:
DEFAULT_CONFIG: dict = {
"sprite_packer.enable": True,
"sprite_packer.dir_key": True,
"sprite_packer.dir_lookup": "",
"sprite_packer.rel_dir_key": False
}
config: dict = DEFAULT_CONFIG.copy()
@staticmethod
def load_config_info() -> dict:
loaded_json: dict = Json.loads(FileHandler.read_file(FILE.CONFIG))
for key in loaded_json.keys():
ConfigHandler.config[key] = loaded_json[key]
@staticmethod
def make_config_info() -> None:
default_config_json: str = Json.dumps(ConfigHandler.DEFAULT_CONFIG, indent=2)
FileHandler.write_file(FILE.CONFIG, default_config_json)
@staticmethod
def get_value(key: str) -> Union[int, bool, str]:
return ConfigHandler.config[key]
DEPENENCIES = """
graphics = class extends screen
_translation = object
x = 0
y = 0
end
_anchor = object
x = 0
y = 0
end
_scale = object
x = 0
y = 0
end
_rotation = 0
fillRect = function(x,y,w,h,c)
super(x,-y,w,h,c)
end
setLinearGradient = function(x1, y1, x2, x2, y2, color1, color2)
super(x1, -y1, x2, -y2, color1, color2)
end
setRadialGradient = function(x, y, radius, color1, color2)
super(x, -y, radius, color1, color2)
end
fillRect = function(x, y, width, height, color)
super(x, -y, width, height, color)
end
fillRoundRect = function(x, y, width, height, radius, color)
super(x, -y, width, height, radius, color)
end
drawSprite = function(sprite, x, y, width = -1, height = -1)
if sprite.startsWith("@") then
local lookup = sprite_lookup[sprite.substring(1, sprite.length)]
local _w = if width == -1 then lookup[3] else width end
local _h = if height == -1 then _w/lookup[3]*lookup[4] else height end
local old_rot = _rotation
local old_anchor_x = _anchor.x
local old_anchor_y = _anchor.y
local old_scale_x = _scale.x
local old_scale_y = _scale.y
if lookup[5] then
setDrawRotation(old_rot-90)
setDrawAnchor(-old_anchor_y, -old_anchor_x)
setDrawScale(old_scale_y, old_scale_x)
end
drawSpritePart(
lookup[0],
lookup[1],
lookup[2],
lookup[3],
lookup[4],
x,
y,
_w,
_h
)
setDrawRotation(old_rot)
setDrawAnchor(old_anchor_x, -old_anchor_y)
setDrawScale(old_scale_x, old_scale_y)
return
end
super(sprite, x, -y, width, height)
end
drawSpritePart = function(sprite, part_x, part_y, part_width, part_height, x, y, width, height)
super(sprite, part_x, part_y, part_width, part_height, x, -y, width, height)
end
drawMap = function(map, x, y, width, height)
super(map, x, -y, width, height)
end
drawText = function(text, x, y, size, color)
super(text, x, -y, size, color)
end
drawLine = function(x1, y1, x2, y2, color)
super(x1, -y1, x2, -y2, color)
end
drawPolygon = function(_points, color)
local points = _points.concat([])
for i=1 to points.length-1 by 2
points[i] = -points[i]
end
super(points, color)
end
fillPolygon = function(_points, color)
local points = _points.concat([])
for i=1 to points.length-1 by 2
points[i] = -points[i]
end
super(points, color)
end
setTranslation = function(x,y)
_translation.x = x-width/2
_translation.y = -y+height/2
super(_translation.x, _translation.y)
end
setDrawAnchor = function(x,y)
_anchor.x = x
_anchor.y = -y
super(_anchor.x,_anchor.y)
end
setDrawScale = function(x, y)
_scale.x = x
_scale.y = y
super(x, y)
end
setDrawRotation = function(ang)
_rotation = ang
super(ang)
end
end
graphics.setDrawAnchor(-1, -1)
graphics.setTranslation(0, 0)
graphics.setDrawScale(1, 1)
"""
def iterate_listdir(scanned_dir: str = None) -> List[str]:
out: List[str] = []
for file_key in OS.listdir(scanned_dir or None):
file_ext: str = OS.path.splitext(file_key)[-1]
file_dir: str = OS.path.normpath(OS.path.join(scanned_dir or "", file_key))
if file_ext == "":
out += iterate_listdir(file_dir)
else:
out.append(file_dir)
return out
def make_var_name_safe(s: str) -> str:
#https://stackoverflow.com/questions/3303312/how-do-i-convert-a-string-to-a-valid-variable-name-in-python
# Remove invalid characters
s = re.sub('[^0-9a-zA-Z_]', '', s)
# Remove leading characters until we find a letter or underscore
s = re.sub('^[^a-zA-Z_]+', '', s)
return s
def generate_commentbar(text: str) -> str:
return "//"+"="*78 + "\n// "+text + "\n//"+"-"*78 + "\n"
class FileHandler:
@staticmethod
def read_file(filename: str) -> str:
filehandle: io.TextIOWrapper = open(filename)
out: str = filehandle.read()
filehandle.close()
return out
@staticmethod
def write_file(filename: str, data: str) -> None:
filehandle: io.TextIOWrapper = open(filename, "w")
filehandle.write(data)
filehandle.close()
class FileConverters:
@staticmethod
def Microscript(data: str, file_name: str) -> str:
return f"{data}\n"
@staticmethod
def Json(data: Union[str, dict, list], file_name = "", is_first_call: bool = True, tabs: int = 0) -> str:
tabber: LambdaType = lambda x: "\t"*x
out: str = ""
_tabs: int = tabs
if isinstance(data, str) and is_first_call:
var_name: str = make_var_name_safe(file_name)
msobj_from_json: str = FileConverters.Json(Json.loads(data), "", False)
return f"{var_name} = {msobj_from_json}"
elif isinstance(data, dict):
out += "object\n"
_tabs += 1
for key in data:
converted_value: str = FileConverters.Json(data[key], is_first_call = False, tabs = _tabs)
out += f"{tabber(_tabs)}\"{key}\" = {converted_value}"
_tabs -= 1
out += f"{tabber(_tabs)}end\n"
elif isinstance(data, list):
out += "[\n"
_tabs += 1
for value in data:
converted_value: str = re.sub(r'^\t+', '', FileConverters.Json(value, is_first_call = False, tabs = _tabs)[:-1])
out += f"{tabber(_tabs)}{converted_value},\n"
_tabs -= 1
out += f"{tabber(_tabs)}]\n"
elif isinstance(data, str):
if str(data)[0] == "@":
out += f"{data[1:]}\n"
else:
out += f"\"{data}\"\n"
elif isinstance(data, bool):
out += f"{str(data).lower()}\n"
else:
out += f"{data}\n"
return out
extensions = [".lua", ".json"]
switcher = {
".lua": Microscript.__func__,
".json": Json.__func__
}
def compile_sprites(rel_dir: str) -> dict:
packer: Packer = newPacker()
png_dir_list: List[str] = []
png_w_list: List[int] = []
png_shrink_list: List[float] = []
bin_img_list: List[Image] = []
sprite_lookup: dict = {}
png_dir_list = list(filter(
lambda x: OS.path.splitext(x)[-1] == ".png" and x.split(OS.path.altsep)[0] != DIR.OUTPUT,
iterate_listdir(rel_dir)
))
if len(png_dir_list) == 0:
return
for idx, file_dir in enumerate(png_dir_list):
img: Image = Image.open(file_dir)
shrink: int = max(1, max(*img.size)/256)
#assert(max(*img.size) <= 256)
img = img.resize((int(img.size[0]/shrink), int(img.size[1]/shrink)))
png_w_list.append(img.size[0])
png_shrink_list.append(shrink)
packer.add_rect(*img.size, idx)
packer.add_bin(256, 256, count = float("inf"))
packer.pack()
for _i in range(len(packer)):
bin_img: Image = Image.new('RGBA', (256, 256), (0, 0, 0, 0))
bin_img_list.append(bin_img)
for rect in packer.rect_list():
b, x, y, w, h, rid = rect
is_rotated: bool = (w != png_w_list[rid])
shrink: int = png_shrink_list[rid]
rect_img: Image = Image.open(png_dir_list[rid])
if is_rotated:
rect_img = rect_img.transpose(Image.ROTATE_90)
rect_img = rect_img.resize((w, h))
bin_img_list[b].paste(rect_img, (x, y))
spr_name: str = OS.path.splitext(png_dir_list[rid])[-2]
if ConfigHandler.get_value("sprite_packer.rel_dir_key"):
spr_name = OS.path.relpath(spr_name, rel_dir)
if not ConfigHandler.get_value("sprite_packer.dir_key"):
spr_name = spr_name.split(OS.path.altsep)[-1]
sprite_lookup[spr_name] = [f"spritesheet_{b}", x, y, w, h, is_rotated]
for bin_img_idx, bin_img in enumerate(bin_img_list):
bin_img.save(OS.path.join(DIR.OUTPUT, f"spritesheet_{bin_img_idx}.png"))
return sprite_lookup
def main() -> None:
current_directory: str
source_output_data: str
OS.path.altsep = "\\"
OS.makedirs(DIR.OUTPUT, exist_ok=True)
current_directory = OS.path.dirname(OS.path.abspath(__file__))
OS.chdir(current_directory)
try:
ConfigHandler.load_config_info()
except FileNotFoundError:
ConfigHandler.make_config_info()
ConfigHandler.load_config_info()
source_output_data = ""
source_output_data += MODULE_COMMENT + "\n\n"
for file_dir in iterate_listdir():
file_ext: str = OS.path.splitext(file_dir)[-1]
is_ext_supported: bool = (file_ext in FileConverters.extensions)
excluded_directory_list: List[str] = [value for value in DIR.__dict__.values()]
is_directory_included: bool = (not file_dir.split(OS.path.altsep)[0] in excluded_directory_list)
if is_ext_supported and is_directory_included:
converter_func: FunctionType = FileConverters.switcher.get(file_ext)
file_name: str = OS.path.splitext(file_dir)[-2]
file_data: str = FileHandler.read_file(file_dir)
file_converted_data: str = converter_func(file_data, file_name)
source_output_data += generate_commentbar(file_dir)
source_output_data += f"{file_converted_data}\n"
if ConfigHandler.get_value("sprite_packer.enable"):
dir_lookup: str = ConfigHandler.get_value("sprite_packer.dir_lookup")
if not OS.path.isdir(dir_lookup):
dir_lookup = ""
sprite_lookup_data: str = FileConverters.Json(compile_sprites(dir_lookup))
if sprite_lookup_data != "None\n":
source_output_data += generate_commentbar("AUTO-GENERATED SPRITE LOOKUP DATA")
source_output_data += f"sprite_lookup = {sprite_lookup_data}"
source_output_data += generate_commentbar("DEPENDENCIES")
source_output_data += DEPENENCIES
FileHandler.write_file(FILE.SOURCE, source_output_data)
pyperclip.copy(source_output_data)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment