Rewrote with charaIds, fixed big sprites
Some checks failed
/ mypy (push) Failing after 19s
/ pylint (push) Successful in 15s
/ release (push) Successful in 6s
/ lint-and-typing (push) Failing after 19s
/ build-artifacts (push) Has been skipped
/ publish-artifacts (push) Has been skipped

This commit is contained in:
Firq 2024-10-11 01:04:05 +02:00
parent ad4391caea
commit 0377c6282a
Signed by: Firq
GPG key ID: 3ACC61C8CEC83C20
6 changed files with 111 additions and 80 deletions

2
.gitignore vendored
View file

@ -1,7 +1,7 @@
# Dev environment # Dev environment
*venv/ *venv/
.vscode/ .vscode/
.out/ *out/
# python files # python files
__pycache__/ __pycache__/

View file

@ -1,25 +1,63 @@
from typing import List, Tuple, TypedDict import pathlib
from typing import Annotated, List, NotRequired, Tuple, TypedDict
import requests import requests
from ..config import AtlasDefaults, ExpressionDefaults, Paths from ..config import AtlasDefaults, Paths, ExpressionDefaults
class SpritesheetData(TypedDict): class SpritesheetData(TypedDict):
facesize: int facesize: Tuple[int, int]
position: Tuple[int, int] position: Tuple[int, int]
class ReturnData(TypedDict): class ExtendData(TypedDict):
id: str faceSizeRect: NotRequired[Annotated[List[int], 2]]
faceX: int faceSize: NotRequired[int]
faceY: int
extendData: dict[str, int]
def fetch_config(chara_id: str) -> SpritesheetData:
url = f"https://api.atlasacademy.io/raw/JP/svtScript?charaId={chara_id}"
def fetch_expression_sheets(servantid: int, imageid: str): response = requests.get(url, timeout=AtlasDefaults.TIMEOUT)
if not response.ok:
raise ValueError(f"{response.status_code} - {response.text}")
resp_data = response.json()[0]
extend_data: ExtendData = resp_data["extendData"]
if "faceSizeRect" in extend_data:
facesize: Tuple[int, int] = tuple(extend_data["faceSizeRect"]) # type: ignore
else:
facesize = tuple(2 * [ extend_data.get("faceSize", ExpressionDefaults.FACESIZE) ]) # type: ignore
position: tuple[int, int] = (resp_data["faceX"], resp_data["faceY"])
returndata: SpritesheetData = {
"facesize": facesize,
"position": position
}
return returndata
def fetch_mstsvtjson():
url = AtlasDefaults.MST_SVT_JSON
filelocation = Paths.IMAGES / "mstsvt.json"
if filelocation.exists():
print("Found cached asset for mstsvt.json")
return
with open(filelocation, 'wb') as handle:
response = requests.get(url, stream=True, timeout=AtlasDefaults.TIMEOUT)
status = response.status_code
if status != 200:
raise ValueError("Could not fetch mstsvnt.json from atlas - please check your network connection")
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
def fetch_expression_sheets(basefolder: str, imageid: str):
atlasurl_base = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}" atlasurl_base = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}"
savefolder = Paths.IMAGES / str(servantid) / imageid savefolder = Paths.IMAGES / basefolder / str(imageid)
if not savefolder.is_dir(): if not savefolder.is_dir():
savefolder.mkdir(exist_ok=True, parents=True) savefolder.mkdir(exist_ok=True, parents=True)
@ -56,33 +94,20 @@ def fetch_expression_sheets(servantid: int, imageid: str):
p = savefolder / f"{idx}.png" p = savefolder / f"{idx}.png"
p.unlink(missing_ok=True) p.unlink(missing_ok=True)
return savefolder
def fetch_info(servantid: int):
atlasurl = f"https://api.atlasacademy.io/basic/{AtlasDefaults.REGION}/servant/{servantid}?lang=en" def fetch_data(servantid: int) -> List[str]:
atlasurl = f"https://api.atlasacademy.io/nice/{AtlasDefaults.REGION}/servant/{servantid}?lore=false&lang=en"
response = requests.get(atlasurl, timeout=AtlasDefaults.TIMEOUT) response = requests.get(atlasurl, timeout=AtlasDefaults.TIMEOUT)
if not response.ok: if not response.ok:
print(response) raise ValueError(f"{response.status_code} - {response.text}")
data = response.json() responsedata = response.json()
print(f"Fetching data and sprites for {data['name']} (ID: {servantid})") svtname = responsedata["name"]
charascripts: List[dict[str, str]] = responsedata["charaScripts"]
chara_ids: List[str] = [chara["id"] for chara in charascripts]
def fetch_data(servantid: int) -> dict[str, SpritesheetData]: print(f"{svtname} ({servantid}) - {len(chara_ids)} charaIds")
fetch_info(servantid) return chara_ids
atlasurl = f"https://api.atlasacademy.io/raw/{AtlasDefaults.REGION}/servant/{servantid}?lore=false&expand=true&lang=en"
response = requests.get(atlasurl, timeout=AtlasDefaults.TIMEOUT)
if not response.ok:
print(response)
data: List[ReturnData] = response.json()["mstSvtScript"]
spritesheet_data: dict[str, SpritesheetData] = {
str(spritesheet["id"]): {
"facesize": spritesheet["extendData"].get("faceSize", ExpressionDefaults.FACESIZE),
"position": (spritesheet["faceX"], spritesheet["faceY"])
} for spritesheet in data
}
print(f"Found a total of {len(spritesheet_data)} spritesheets")
return spritesheet_data

View file

@ -6,46 +6,45 @@ from PIL import Image
from tqdm.contrib import itertools as tqdm_itertools from tqdm.contrib import itertools as tqdm_itertools
from ..config import Paths from ..config import Paths
from .atlas import SpritesheetData, fetch_data, fetch_expression_sheets from .atlas import SpritesheetData, fetch_data, fetch_expression_sheets, fetch_config
def compose(input_id: int, filters: Optional[List[str]] = None):
def compose(servantid: int, filters: Optional[List[str]] = None):
Paths.IMAGES.mkdir(exist_ok=True) Paths.IMAGES.mkdir(exist_ok=True)
Paths.OUTPUT.mkdir(exist_ok=True) Paths.OUTPUT.mkdir(exist_ok=True)
sprites = fetch_data(servantid) if input_id < 10000:
chara_ids = fetch_data(input_id)
savefolder = Paths.OUTPUT / str(input_id)
else:
print(f"Processing manually uploaded charaId {input_id}")
savefolder = Paths.OUTPUT / "manual"
chara_ids = [input_id]
if not savefolder.is_dir():
savefolder.mkdir(parents=True, exist_ok=True)
if filters is not None: if filters is not None:
sprites = { key: value for key, value in sprites.items() if key in filters } chara_ids = [ v for v in chara_ids if v in filters ]
for sprite in sprites: for char_id in chara_ids:
fetch_expression_sheets(servantid, sprite) expfolder = fetch_expression_sheets(savefolder.stem, char_id)
config = fetch_config(char_id)
process_sprite(expfolder, config, savefolder)
path = Paths.IMAGES / str(servantid) print(f"Files have been saved at: {savefolder.absolute()}")
savefolder = Paths.OUTPUT / str(servantid)
if not savefolder.is_dir():
savefolder.mkdir()
for f in path.iterdir():
if filters is not None and str(f.stem) not in filters:
continue
process_sprite(f, servantid, sprites[f.stem])
print(f"Files have been saved at: {(Paths.OUTPUT / str(servantid)).absolute()}")
def calculate_counts(width: int, height: int, facesize: int): def calculate_counts(width: int, height: int, facesize: tuple[int, int]):
return height // facesize, width // facesize return height // facesize[1], width // facesize[0]
def gen_main_sprite(folder: pathlib.Path):
image = Image.open(folder / "0.png")
width, height = image.size
return image.crop((0, 0, width, height - 256))
def process_sprite(images_folder: pathlib.Path, servantid: int, configdata: SpritesheetData): def process_sprite(images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path):
main = Image.open(images_folder / "0.png") main_sprite = gen_main_sprite(images_folder)
save_sprite(main_sprite, outputfolder, f"{images_folder.stem}")
width, _ = main.size
main_sprite = main.crop((0, 0, width, 756))
save_sprite(main_sprite, servantid, f"{images_folder.stem}")
for i in images_folder.iterdir(): for i in images_folder.iterdir():
initial_row, index = 0, int(i.stem) initial_row, index = 0, int(i.stem)
@ -53,22 +52,24 @@ def process_sprite(images_folder: pathlib.Path, servantid: int, configdata: Spri
rowcount, colcount = calculate_counts(*expressions.size, configdata["facesize"]) rowcount, colcount = calculate_counts(*expressions.size, configdata["facesize"])
if i.name == "0.png": if i.name == "0.png" and 256 < configdata["facesize"][1]:
initial_row = 3 continue
elif i.name == "0.png":
initial_row = rowcount - 1
for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-="): for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-="):
img = generate_sprite(main_sprite, expressions, x, y, configdata) img = generate_sprite(main_sprite, expressions, x, y, configdata)
if img is not None: if img is not None:
save_sprite(img, servantid, f"{images_folder.stem}", (x, y, colcount, index)) save_sprite(img, outputfolder, f"{images_folder.stem}", (x, y, colcount, index))
def generate_sprite(main_sprite: Image.Image, expressions: Image.Image, row: int, col: int, configdata: SpritesheetData) -> Image.Image | None: def generate_sprite(main_sprite: Image.Image, expressions: Image.Image, row: int, col: int, configdata: SpritesheetData) -> Image.Image | None:
position, facesize = configdata["position"], configdata["facesize"] position, facesize = configdata["position"], configdata["facesize"]
roi = ( roi = (
col * facesize, col * facesize[0],
row * facesize, row * facesize[1],
(col + 1) * facesize - 1, (col + 1) * facesize[0] - 1,
(row + 1) * facesize - 1 (row + 1) * facesize[1] - 1
) )
expression = expressions.crop(roi) expression = expressions.crop(roi)
@ -81,8 +82,8 @@ def generate_sprite(main_sprite: Image.Image, expressions: Image.Image, row: int
return composition return composition
def save_sprite(image: Image.Image, folder_id: int, name: str, info: tuple | None = None): def save_sprite(image: Image.Image, outputfolder: pathlib.Path, name: str, info: tuple | None = None):
savefolder = Paths.OUTPUT / str(folder_id) / name savefolder = outputfolder / name
if not savefolder.is_dir(): if not savefolder.is_dir():
savefolder.mkdir() savefolder.mkdir()
@ -97,6 +98,8 @@ def save_sprite(image: Image.Image, folder_id: int, name: str, info: tuple | Non
raise ValueError("Should not have any faces") raise ValueError("Should not have any faces")
elif column_count == 4: elif column_count == 4:
postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1) + column_count) postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1) + column_count)
elif column_count == 1:
postfix = str((file_idx - 1) * 16 + 1 if file_idx >= 2 else file_idx * 4 + 1)
elif column_count < 4: elif column_count < 4:
postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1)) postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1))
else: else:

View file

@ -56,9 +56,9 @@ def run_cli():
welcome() welcome()
servantid = input("Enter servant ID: ") input_id = input("Enter servantId/charaId: ")
try: try:
t = int(servantid) t = int(input_id)
if t <= 0: if t <= 0:
raise ValueError raise ValueError
except ValueError: except ValueError:
@ -66,11 +66,13 @@ def run_cli():
sys.exit(1) sys.exit(1)
if args.cacheclear: if args.cacheclear:
cachepath = Paths.IMAGES / str(servantid) cachepath = Paths.IMAGES / str(input_id)
if input_id > 10000:
cachepath = Paths.IMAGES / "manual" / str(input_id)
if cachepath.exists(): if cachepath.exists():
rmdir(cachepath) rmdir(cachepath)
print("Successfully cleared cached assets") print("Successfully cleared cached assets")
else: else:
print("No cache to clear was found, continuing") print("No cache to clear was found, continuing")
compose(servantid, args.filter) compose(int(input_id), args.filter)

View file

@ -14,3 +14,4 @@ class ExpressionDefaults:
class AtlasDefaults: class AtlasDefaults:
REGION = "JP" REGION = "JP"
TIMEOUT = 10 TIMEOUT = 10
MST_SVT_JSON = "https://git.atlasacademy.io/atlasacademy/fgo-game-data/raw/branch/JP/master/mstSvtScript.json"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "atlasimagecomposer" name = "atlasimagecomposer"
version = "0.1.0-c.1" version = "0.1.0-c.2"
dependencies = [ dependencies = [
"numpy~=2.0.1", "numpy~=2.0.1",
"pillow~=10.4.0", "pillow~=10.4.0",
@ -10,7 +10,7 @@ dependencies = [
requires-python = ">= 3.10" requires-python = ">= 3.10"
authors = [{name = "Firq", email = "firelp42@gmail.com"}] authors = [{name = "Firq", email = "firelp42@gmail.com"}]
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
description = "Package that enables peopßle to quickly download and generate all potential spritesheet expressions with a single command" description = "Package that enables people to quickly download and generate all potential spritesheet expressions with a single command"
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",