Rewrote with charaIds, fixed big sprites
This commit is contained in:
parent
ad4391caea
commit
0377c6282a
6 changed files with 111 additions and 80 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,7 +1,7 @@
|
||||||
# Dev environment
|
# Dev environment
|
||||||
*venv/
|
*venv/
|
||||||
.vscode/
|
.vscode/
|
||||||
.out/
|
*out/
|
||||||
|
|
||||||
# python files
|
# python files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue