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
*venv/
.vscode/
.out/
*out/
# python files
__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
from ..config import AtlasDefaults, ExpressionDefaults, Paths
from ..config import AtlasDefaults, Paths, ExpressionDefaults
class SpritesheetData(TypedDict):
facesize: int
facesize: Tuple[int, int]
position: Tuple[int, int]
class ReturnData(TypedDict):
id: str
faceX: int
faceY: int
extendData: dict[str, int]
class ExtendData(TypedDict):
faceSizeRect: NotRequired[Annotated[List[int], 2]]
faceSize: NotRequired[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}"
savefolder = Paths.IMAGES / str(servantid) / imageid
savefolder = Paths.IMAGES / basefolder / str(imageid)
if not savefolder.is_dir():
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.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)
if not response.ok:
print(response)
raise ValueError(f"{response.status_code} - {response.text}")
data = response.json()
print(f"Fetching data and sprites for {data['name']} (ID: {servantid})")
responsedata = response.json()
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]:
fetch_info(servantid)
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
print(f"{svtname} ({servantid}) - {len(chara_ids)} charaIds")
return chara_ids

View file

@ -6,46 +6,45 @@ from PIL import Image
from tqdm.contrib import itertools as tqdm_itertools
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(servantid: int, filters: Optional[List[str]] = None):
def compose(input_id: int, filters: Optional[List[str]] = None):
Paths.IMAGES.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:
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:
fetch_expression_sheets(servantid, sprite)
for char_id in chara_ids:
expfolder = fetch_expression_sheets(savefolder.stem, char_id)
config = fetch_config(char_id)
process_sprite(expfolder, config, savefolder)
path = Paths.IMAGES / str(servantid)
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()}")
print(f"Files have been saved at: {savefolder.absolute()}")
def calculate_counts(width: int, height: int, facesize: int):
return height // facesize, width // facesize
def calculate_counts(width: int, height: int, facesize: tuple[int, int]):
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):
main = Image.open(images_folder / "0.png")
width, _ = main.size
main_sprite = main.crop((0, 0, width, 756))
save_sprite(main_sprite, servantid, f"{images_folder.stem}")
def process_sprite(images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path):
main_sprite = gen_main_sprite(images_folder)
save_sprite(main_sprite, outputfolder, f"{images_folder.stem}")
for i in images_folder.iterdir():
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"])
if i.name == "0.png":
initial_row = 3
if i.name == "0.png" and 256 < configdata["facesize"][1]:
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="-="):
img = generate_sprite(main_sprite, expressions, x, y, configdata)
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:
position, facesize = configdata["position"], configdata["facesize"]
roi = (
col * facesize,
row * facesize,
(col + 1) * facesize - 1,
(row + 1) * facesize - 1
col * facesize[0],
row * facesize[1],
(col + 1) * facesize[0] - 1,
(row + 1) * facesize[1] - 1
)
expression = expressions.crop(roi)
@ -81,8 +82,8 @@ def generate_sprite(main_sprite: Image.Image, expressions: Image.Image, row: int
return composition
def save_sprite(image: Image.Image, folder_id: int, name: str, info: tuple | None = None):
savefolder = Paths.OUTPUT / str(folder_id) / name
def save_sprite(image: Image.Image, outputfolder: pathlib.Path, name: str, info: tuple | None = None):
savefolder = outputfolder / name
if not savefolder.is_dir():
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")
elif column_count == 4:
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:
postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1))
else:

View file

@ -56,9 +56,9 @@ def run_cli():
welcome()
servantid = input("Enter servant ID: ")
input_id = input("Enter servantId/charaId: ")
try:
t = int(servantid)
t = int(input_id)
if t <= 0:
raise ValueError
except ValueError:
@ -66,11 +66,13 @@ def run_cli():
sys.exit(1)
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():
rmdir(cachepath)
print("Successfully cleared cached assets")
else:
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:
REGION = "JP"
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]
name = "atlasimagecomposer"
version = "0.1.0-c.1"
version = "0.1.0-c.2"
dependencies = [
"numpy~=2.0.1",
"pillow~=10.4.0",
@ -10,7 +10,7 @@ dependencies = [
requires-python = ">= 3.10"
authors = [{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 = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",