1
0
mirror of https://github.com/Bluemangoo/sekai-unpacker.git synced 2026-05-06 20:44:47 +08:00

Compare commits

...

4 Commits

Author SHA1 Message Date
e019948017
dockerfile, modify it when using 2026-04-23 19:42:09 +08:00
e00b25b6c7
fix tcp server wait 2026-04-23 19:00:05 +08:00
59c50d5e08
fix path sep 2026-04-23 18:31:54 +08:00
b892c3b46f
vibe config example 2026-04-23 16:08:41 +08:00
9 changed files with 664 additions and 13 deletions

20
.dockerignore Normal file
View File

@ -0,0 +1,20 @@
# Git files
.git
.gitignore
# Build artifacts
target/
# Logs
logs/
*.log
# Data
data/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~

20
client.DOCKERFILE Normal file
View File

@ -0,0 +1,20 @@
FROM rust:slim-bookworm AS builder
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y musl-tools pkg-config libssl-dev
RUN rustup target add x86_64-unknown-linux-musl
COPY . .
RUN cargo build --target x86_64-unknown-linux-musl --release --bin client
FROM alpine:latest
RUN apk add --no-cache ca-certificates
RUN update-ca-certificates
WORKDIR /app
COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/client .
# Copy config file
COPY sekai-unpacker-client.yaml .
CMD ["./client", "-p", "cn", "-p", "jp"]
EXPOSE 3000

View File

@ -67,6 +67,7 @@ impl Profile {
from obj; from obj;
self.sync_context.asset_version = asset_version as String; self.sync_context.asset_version = asset_version as String;
self.sync_context.asset_hash = asset_hash as String; self.sync_context.asset_hash = asset_hash as String;
self.sync_context.app_version = app_version as String;
self.concurrent = concurrent as Number.as_u64().ok_or(anyhow::Error::msg("concurrent is not usize"))? as usize; self.concurrent = concurrent as Number.as_u64().ok_or(anyhow::Error::msg("concurrent is not usize"))? as usize;
} }
Ok(()) Ok(())

View File

@ -200,6 +200,16 @@ async fn main() -> anyhow::Result<()> {
if error.to_string().contains(REGION_NOT_FOUND) { if error.to_string().contains(REGION_NOT_FOUND) {
return; return;
} }
if error
.to_string()
.contains("Session did not reconnect within 15s")
{
error!(
"Session lost for profile {}. Waiting for a fresh tunnel endpoint...",
profile.0
);
return;
}
error!("{}", error); error!("{}", error);
} }
_ => {} _ => {}
@ -251,6 +261,16 @@ async fn main() -> anyhow::Result<()> {
.await; .await;
} }
Err(error) => { Err(error) => {
if error
.to_string()
.contains("Session did not reconnect within 15s")
{
error!(
"Session lost for profile {}. Reconnecting tunnel...",
profile.0
);
break;
}
error!("{}", error); error!("{}", error);
} }
_ => {} _ => {}

View File

@ -2,6 +2,7 @@ use crate::config::Profile;
use crate::http::{close, download, sync}; use crate::http::{close, download, sync};
use common::http::{CloseRequest, DownloadRequest}; use common::http::{CloseRequest, DownloadRequest};
use communicator::ClientManager; use communicator::ClientManager;
use anyhow::anyhow;
use log::{error, info}; use log::{error, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -64,30 +65,42 @@ pub async fn run(
id: id.clone(), id: id.clone(),
task: task.clone(), task: task.clone(),
}; };
let result = download(&mut client.get_client().await.unwrap(), &req, &p1).await; let mut conn = client.get_client().await?;
if let Err(e) = result let mut result = download(&mut conn, &req, &p1).await;
&& let Some(_) = e.downcast_ref::<h2::Error>() if let Err(e) = &result
&& e.downcast_ref::<h2::Error>().is_some()
{ {
download(&mut client.get_client().await.unwrap(), &req, &p1) let mut retry_conn = client.get_client().await?;
.await result = download(&mut retry_conn, &req, &p1).await;
.unwrap();
} }
result?;
local_manifest local_manifest
.add_bundle(task.bundle_path.clone(), task.bundle_hash.clone()) .add_bundle(task.bundle_path.clone(), task.bundle_hash.clone())
.await .await
.unwrap(); ?;
drop(permit); drop(permit);
Ok::<(), anyhow::Error>(())
}); });
} }
let mut succeed = 0; let mut succeed = 0;
let mut failed = 0; let mut failed = 0;
while let Some(r) = join_set.join_next().await { while let Some(r) = join_set.join_next().await {
if let Err(e) = r { match r {
error!("{}", e); Ok(Ok(())) => {
failed += 1; succeed += 1;
} else { }
succeed += 1; Ok(Err(e)) => {
if e.to_string().contains("Session did not reconnect within 15s") {
return Err(anyhow!(e));
}
error!("{}", e);
failed += 1;
}
Err(e) => {
error!("{}", e);
failed += 1;
}
} }
} }
local_manifest.save().await?; local_manifest.save().await?;

View File

@ -18,7 +18,8 @@ pub async fn server_send_files<P: AsRef<Path>, S: AsRef<str>>(
let metadata = file.metadata().await?; let metadata = file.metadata().await?;
let file_name = &path_ref.1; let file_name = &path_ref.1;
let name_bytes = file_name.as_ref().as_bytes(); let file_name = file_name.as_ref().replace('\\', "/");
let name_bytes = file_name.as_bytes();
let file_size = metadata.len(); let file_size = metadata.len();
let mut meta_buf = BytesMut::with_capacity(2 + name_bytes.len() + 8); let mut meta_buf = BytesMut::with_capacity(2 + name_bytes.len() + 8);
@ -62,6 +63,7 @@ pub async fn client_receive(
let mut name_buf = vec![0u8; name_len as usize]; let mut name_buf = vec![0u8; name_len as usize];
response_body_reader.read_exact(&mut name_buf).await?; response_body_reader.read_exact(&mut name_buf).await?;
let file_name = String::from_utf8(name_buf)?; let file_name = String::from_utf8(name_buf)?;
let file_name = file_name.replace('\\', "/");
let data_len = response_body_reader.read_u64().await?; let data_len = response_body_reader.read_u64().await?;

View File

@ -0,0 +1,249 @@
# ==========================================
# Sekai Unpacker 客户端配置示例
# sekai-unpacker-client.example.yaml
# ==========================================
# 日志级别: DEBUG, INFO, WARN, ERROR
log_level: "INFO"
# 说明:`client`/`server` 是 TCP 连接方向,不是进程身份。
# - `client`: 主动连出到远端
# - `server`: 本地监听并接受远端连入
# 当前文件默认展示“client 进程常用连出模式”,也可同时/单独启用 `server`。
# ==========================================
# 服务器连接配置
# ==========================================
# 可配置多个服务器,客户端会依次尝试连接
client:
# 第一个服务器配置
- url: "127.0.0.1:3333" # 服务器地址和端口
token: "your_auth_token_here" # 认证令牌,必须与服务器配置相同
# host: "example.com" # [可选] TLS SNI 主机名
# 仅在需要 TLS 加密到 Identity 交换为止时配置
# 如不配置,则使用明文握手
# 加密范围Identity 交换之前的握手H2 通信不加密
# 可以配置多个服务器
# - url: "backup.example.com:3333"
# token: "backup_token"
# host: "backup.example.com"
# ==========================================
# 可选:反向连接场景(本进程作为监听端)
# ==========================================
# server:
# - url: "0.0.0.0:3334" # 本地监听地址和端口
# token: "reverse_link_token" # 需与对端 client.token 一致
# # cert: "/path/to/certificate.pem" # [可选] 启用 TLS 握手
# # key: "/path/to/private.key"
# ==========================================
# 同步配置文件Profiles
# ==========================================
# 每个配置文件对应一个地区的同步任务
profiles:
# ==========================================
# 国服配置示例
# ==========================================
cn:
# [必需] 地区标识,必须与服务器配置中的 regions 一致
region: cn
# ========== 版本信息配置(可选) ==========
# 以下三个字段会被 dynamic_load 中的值覆盖
# 应用版本
# app_version: "6.0.0"
# 资源版本
# asset_version: "1.0.0"
# 资源哈希值
# asset_hash: "abc123def456"
# ========== 动态加载配置(可选) ==========
# 从指定 URL 动态加载版本和哈希信息
# 用于获取最新版本而无需更新配置文件
dynamic_load:
url: "https://api.example.com/versions/cn"
# 映射关系:本地字段名 -> 远程 JSON 字段名
map:
app_version: appVersion
asset_version: assetVersion
asset_hash: assetHash
# concurrent: concurrent # [可选] 远程并发数,需为数字
# ========== 下载配置 ==========
# 更新间隔(秒,可选)
# 如设置,则每隔指定秒数自动检查更新
# interval: 3600
# 并发下载数(单个资源包的同时下载线程数)
concurrent: 50
# 精确匹配单文件包
exact_single_file_bundle: true
# 导出基础路径
path: "./data/cn"
# ========== 资源过滤配置 ==========
filters:
# 启动应用时必须下载的资源类型
start_app:
- "thumbnail" # 缩略图
- "stamp" # 邮票/表情
# - "area" # 地图
# - "home" # 主页背景
# 按需下载(用户请求时才下载)的资源类型
on_demand:
- "thumbnail"
- "stamp"
# - "event" # 活动资源
# - "mysekai" # 我的SEKAI资源
# 跳过下载的资源标识
# 完全不下载这些资源
skip: []
# 按文件扩展名过滤
# 仅下载指定扩展名的文件
file_ext: []
# ========== 导出和处理配置 ==========
export:
# 是否按分类导出(按资源类别分文件夹)
by_category: false
# ---- USM 视频文件处理 ----
usm:
export: true # 是否导出 USM 文件
decode: true # 是否解码为视频格式(需要 FFmpeg
# ---- ACB 音频包处理 ----
acb:
export: true # 是否导出 ACB 文件
decode: true # 是否解包并解码(需要 cri_ware_decoder
# ---- HCA 音频文件处理 ----
hca:
decode: true # 是否解码为 MP3/FLAC需要 FFmpeg
# ---- 图片处理 ----
images:
convert_to_webp: false # 是否转换为 WebP 格式(需要 FFmpeg
remove_png: false # 转换后是否删除原 PNG 文件
# ---- 视频处理 ----
video:
convert_to_mp4: false # 是否转换为 MP4 格式(需要 FFmpeg
direct_usm_to_mp4_with_ffmpeg: false # 直接用 FFmpeg 转换 USM 为 MP4不先转M2V
remove_m2v: false # 转换后是否删除原 M2V 文件
# ---- 音频处理 ----
audio:
convert_to_mp3: false # 是否转换为 MP3 格式(需要 FFmpeg
convert_to_flac: false # 是否转换为 FLAC 格式(需要 FFmpeg
remove_wav: false # 转换后是否删除原 WAV 文件
# ==========================================
# 日服配置示例
# ==========================================
jp:
# [必需] 地区标识
region: jp
# 并发下载数
concurrent: 50
# 精确匹配单文件包
exact_single_file_bundle: true
# 导出路径
path: "./data/jp"
# 动态加载配置(使用 GitHub 作为版本源)
dynamic_load:
url: "https://github.com/Team-Haruki/haruki-sekai-master/raw/refs/heads/main/versions/current_version.json"
map:
# app_version: appVersion
asset_version: assetVersion
asset_hash: assetHash
# concurrent: concurrent # [可选] 远程并发数,需为数字
# 过滤配置
filters:
start_app:
- "thumbnail"
- "stamp"
on_demand:
- "thumbnail"
- "stamp"
skip: []
file_ext: []
# 导出配置
export:
by_category: false
usm:
export: true
decode: true
acb:
export: true
decode: true
hca:
decode: true
images:
convert_to_webp: false
remove_png: false
video:
convert_to_mp4: false
direct_usm_to_mp4_with_ffmpeg: false
remove_m2v: false
audio:
convert_to_mp3: false
convert_to_flac: false
remove_wav: false
# ==========================================
# 配置说明
# ==========================================
#
# 【基本配置】
# - log_level: 日志输出级别,建议生产环境设为 INFO
# - client: 主动连出配置,支持故障转移
# - server: 本地监听配置(可选,反向连接时使用)
#
# 【版本管理】
# - 优先级直接配置app_version等> dynamic_load > 默认值
# - 建议使用 dynamic_load 保持版本自动更新
#
# 【资源过滤】
# - start_app: 影响初始化和启动时的下载
# - on_demand: 需要时才下载的资源
# - skip: 完全忽略的资源,不会被下载
# - file_ext: 为空表示下载所有文件,指定则仅下载这些扩展名
#
# 【导出处理】
# - export:true 必须为 true资源才会被导出
# - decode:true 表示对导出的资源进行解码/转换
# - 各转换功能均需要对应的外部工具FFmpeg 等)
#
# 【多地区配置】
# - 可在 profiles 中定义多个地区
# - 每个地区独立管理下载和导出配置
# - region 名称必须与服务器配置一致
#
# 【TLS 加密】
# - 仅在需要加密握手时配置 host 字段
# - 加密仅到 Identity 交换,之后 H2 通信不加密
# - 留空或不配置则使用明文握手
#
# 【角色说明】
# - 角色由连接方向决定,不由可执行文件名称决定
# - 同一进程可只开 `client`、只开 `server`,或两者同时开启

View File

@ -0,0 +1,283 @@
# ==========================================
# Sekai Unpacker 服务器配置示例
# sekai-unpacker-server.example.yaml
# ==========================================
# 日志级别: DEBUG, INFO, WARN, ERROR
log_level: "INFO"
# 说明:`server`/`client` 是 TCP 连接方向,不是进程身份。
# - `server`: 本地监听并接受远端连入
# - `client`: 主动连出到远端
# 当前文件默认展示“server 进程常用监听模式”,也可同时/单独启用 `client`。
# ==========================================
# 服务器监听配置
# ==========================================
# 可配置多个监听地址和端口
server:
# 主服务器配置
- url: "127.0.0.1:3333" # 监听地址和端口
token: "your_secure_token_here" # 认证令牌,对端必须提供相同的令牌
# ========== TLS 证书配置(可选) ==========
# 如配置证书和密钥,服务器将支持 TLS 加密握手
# 如未配置,服务器仅接受明文握手
# 注意:加密仅应用于 Identity 交换阶段H2 通信保持明文
# cert: "/path/to/certificate.pem" # TLS 证书文件路径
# key: "/path/to/private.key" # TLS 私钥文件路径
# 例子:
# cert: "D:\\certs\\_.example.com-chain.pem"
# key: "D:\\certs\\_.example.com-key.pem"
# 可配置多个监听地址和端口
# - url: "0.0.0.0:3334"
# token: "alternative_token"
# cert: "/path/to/another-cert.pem"
# key: "/path/to/another-key.pem"
# ==========================================
# 可选:反向连接场景(本进程主动连出)
# ==========================================
# client:
# - url: "upstream.example.com:3333" # 远端监听地址和端口
# token: "reverse_link_token" # 需与对端 server.token 一致
# # host: "upstream.example.com" # [可选] 启用 TLS 握手时填写
# ==========================================
# 资源执行配置
# ==========================================
execution:
# HTTP 代理设置(可选)
# 如需通过代理服务器访问资源源,配置此项
# proxy: "http://proxy.example.com:8080"
# proxy: "socks5://proxy.example.com:1080"
# 资源下载和处理的超时时间(秒)
timeout_seconds: 300
# 是否允许客户端取消正在执行的任务
allow_cancel: true
# ========== 重试策略 ==========
retry:
# 最大重试次数(网络错误时)
attempts: 4
# 初始退避时间(毫秒)
# 首次失败后等待 1000ms 再重试
initial_backoff_ms: 1000
# 最大退避时间(毫秒)
# 后续每次重试的等待时间翻倍,但不超过此值
max_backoff_ms: 4000
# ==========================================
# 工具配置
# ==========================================
tools:
# ========== FFmpeg 工具 ==========
# 用于视频和音频的转换和处理
# 留空或 "ffmpeg" 表示使用系统 PATH 中的 ffmpeg
# 如 FFmpeg 未在 PATH 中,请指定完整路径
ffmpeg_path: "ffmpeg"
# 例子:
# ffmpeg_path: "C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe"
# ffmpeg_path: "/usr/bin/ffmpeg"
# ========== AssetStudio CLI 工具(可选) ==========
# 用于解析 Unity AssetBundle 文件
# 如不配置,某些资源的精细解析功能可能不可用
# asset_studio_cli_path: "D:\\Workspace\\AssetStudio\\AssetStudioCLI\\bin\\Release\\net9.0\\AssetStudioModCLI.exe"
# asset_studio_cli_path: "/opt/AssetStudioCLI"
# ==========================================
# 并发配置
# ==========================================
# 各处理阶段的并发数量配置
# 数值越大,处理速度越快,但对系统资源占用也越大
concurrency:
# 同时下载的资源文件数(好像没用到)
download: 4
# 同时上传/传输的资源文件数(好像没用到)
upload: 4
# ACB 音频包解析的并发数
acb: 8
# USM 视频解析的并发数
usm: 4
# HCA 音频解码的并发数(通常可设置较大值)
hca: 16
# ==========================================
# 地区配置
# ==========================================
# 定义支持的资源地区及其对应的数据源
regions:
# ==========================================
# 日本服务器配置
# ==========================================
jp:
# 是否启用该地区
enabled: true
# ========== 资源提供者配置 ==========
provider:
# 提供者类型colorful_palette 或 nuverse
kind: colorful_palette
# 资源包元信息 API 地址模板
# {env}环境标识production/staging 等)
# {hash}:配置哈希值
# {asset_version}:资源版本
# {asset_hash}:资源包哈希
asset_info_url_template: "https://{env}-{hash}-assetbundle-info.sekai.colorfulpalette.org/api/version/{asset_version}/{asset_hash}/os/ios"
# 资源包下载地址模板
# {bundle_path}:资源包路径
asset_bundle_url_template: "https://{env}-{hash}-assetbundle.sekai.colorfulpalette.org/{asset_version}/{asset_hash}/ios/{bundle_path}"
# 配置文件名(如 production
profile: "production"
# 配置对应的哈希值映射表
profile_hashes:
production: "cf2d2388"
# staging: "12345678"
# 是否需要 Cookie
# 某些源需要 Cookie 才能访问(防盗链)
required_cookies: true
# 获取 Cookie 的 Bootstrap URL可选
# 如配置,服务器会先访问此 URL 获取 Cookie
# cookie_bootstrap_url: "https://example.com/bootstrap"
# ========== 加密配置 ==========
# 资源包的 AES 加密参数
crypto:
# AES 密钥(十六进制字符串)
aes_key_hex: "6732666343305a637a4e394d544a3631"
# AES 初始向量(十六进制字符串)
aes_iv_hex: "6d737833495630693958453575595a31"
# ========== 运行时配置 ==========
runtime:
# 资源对应的 Unity 版本
# 用于正确解析 AssetBundle
unity_version: "2022.3.21f1"
# ==========================================
# 中国服务器配置
# ==========================================
cn:
enabled: true
provider:
# Nuverse 提供者(国服使用)
kind: nuverse
# 获取资源版本的 API 地址
# {app_version}:应用版本
asset_version_url: "https://lf3-mkcncdn-tos.dailygn.com/obj/rt-game-lf/gdl_app_5236/Mainland/{app_version}/Release/cn_online/ios/version"
# 资源包元信息 API 地址模板
# {asset_version}:资源版本
asset_info_url_template: "https://lf3-mkcncdn-tos.dailygn.com/obj/sf-game-lf/gdl_app_5236/AssetBundle/{app_version}/Release/cn_online/ios{asset_version}/AssetBundleInfoNew.json"
# 资源包下载地址模板
# {app_version}:应用版本
# {bundle_path}:资源包路径
asset_bundle_url_template: "https://lf3-mkcncdn-tos.dailygn.com/obj/sf-game-lf/gdl_app_5236/AssetBundle/{app_version}/Release/cn_online/{bundle_path}"
# 是否需要 Cookie
required_cookies: false
# cookie_bootstrap_url: null
crypto:
aes_key_hex: "6732666343305a637a4e394d544a3631"
aes_iv_hex: "6d737833495630693958453575595a31"
runtime:
unity_version: "2022.3.21f1"
# ==========================================
# 其他地区配置示例(禁用)
# ==========================================
# tw:
# enabled: false
# provider:
# kind: colorful_palette
# asset_info_url_template: "https://{env}-{hash}-assetbundle-info.sekai.colorfulpalette.org/api/version/{asset_version}/{asset_hash}/os/ios"
# asset_bundle_url_template: "https://{env}-{hash}-assetbundle.sekai.colorfulpalette.org/{asset_version}/{asset_hash}/ios/{bundle_path}"
# profile: "production"
# profile_hashes:
# production: "cf2d2388"
# required_cookies: true
# crypto:
# aes_key_hex: "6732666343305a637a4e394d544a3631"
# aes_iv_hex: "6d737833495630693958453575595a31"
# runtime:
# unity_version: "2022.3.21f1"
# ==========================================
# 配置说明
# ==========================================
#
# 【服务器配置】
# - 可配置多个监听地址,支持多个客户端连接
# - 每个监听配置需要不同的 token 来区分认证
# - cert 和 key 配置是可选的:
# * 配置了:支持 TLS 加密握手 + 明文握手(双模式)
# * 未配置:仅支持明文握手
# - 如需主动连出,可额外配置 `client` 区块
#
# 【认证机制】
# - client 中的 token 必须与某个 server 配置中的 token 匹配
# - 握手时会验证 token不匹配则拒绝连接
#
# 【TLS 加密】
# - 加密范围Identity 交换(身份验证)阶段
# - H2 通信(实际数据传输)始终不加密,基于 TCP 流
# - 证书应该是 X.509 格式的 PEM 编码文件
#
# 【资源源配置】
# - 支持两种提供者:
# * colorful_palette日服资源源
# * nuverse国服资源源
# - 每个提供者有不同的 API 地址格式
#
# 【并发控制】
# - 合理设置并发数可提升处理速度
# - 过高可能导致网络压力或系统资源不足
# - 建议download/upload 4-8hca 16其他 4-8
#
# 【版本信息】
# - 客户端会根据 region 字段从服务器获取该地区的配置
# - 确保客户端 profiles 中的 region 值与服务器的地区名称一致
#
# 【禁用地区】
# - 将 enabled 设为 false 可禁用某个地区
# - 禁用的地区客户端无法同步
#
# 【代理配置】
# - 如果服务器需要通过代理访问资源源,在 execution.proxy 中配置
# - 支持 HTTP、HTTPS、SOCKS5 代理
#
# 【工具路径】
# - 确保 ffmpeg 和 AssetStudioCLI 可从配置的路径访问
# - 如在 PATH 中,可直接写命令名称
# - 否则需要提供完整的绝对路径
#
# 【角色说明】
# - 角色由连接方向决定,不由可执行文件名称决定
# - 同一进程可只开 `client`、只开 `server`,或两者同时开启

43
server.DOCKERFILE Normal file
View File

@ -0,0 +1,43 @@
FROM rust:slim-bookworm AS builder
WORKDIR /usr/src/app
RUN cargo build --release
RUN apt-get update && apt-get install -y musl-tools pkg-config libssl-dev
RUN rustup target add x86_64-unknown-linux-musl
COPY . .
RUN cargo build --target x86_64-unknown-linux-musl --release --bin client
FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS assetstudio-builder
WORKDIR /src
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && \
rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --single-branch --branch sekai-modify https://github.com/Team-Haruki/AssetStudio.git
RUN cd AssetStudio/AssetStudioCLI && \
dotnet publish -c Release -r linux-x64 -f net9.0 --self-contained true -o /app/assetstudio \
-p:PublishTrimmed=false \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true
FROM mwader/static-ffmpeg:7.1.1 AS ffmpeg-builder
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
tzdata \
libicu76 \
libxml2 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/server /app/server
COPY --from=assetstudio-builder /app/assetstudio /app/server
COPY --from=ffmpeg-builder /ffmpeg /usr/local/bin/ffmpeg
RUN ln -sf /app/assetstudio/AssetStudioModCLI /app/assetstudio/AssetStudioCLI && \
mkdir -p logs
ENV TZ=Asia/Shanghai \
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
EXPOSE 3000
CMD ["./server"]