Initial commit: NovelasVM platform with multi-engine support and Umineko Web integration
This commit is contained in:
290
bin/build-novela.sh
Executable file
290
bin/build-novela.sh
Executable file
@@ -0,0 +1,290 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# build-novela.sh - Compila y publica una novela visual en NovelasVM
|
||||
# Uso: build-novela.sh <slug> <ruta_proyecto> [motor]
|
||||
# motor: renpy | unity | web | onscripter (si no se indica, se detecta automaticamente)
|
||||
# Ejemplo: build-novela.sh demo /opt/novelas/projects/demo
|
||||
|
||||
NAME="$1"
|
||||
PROJECT="$2"
|
||||
FORCE_ENGINE="$3"
|
||||
|
||||
RENPY_HOME="/opt/novelas/tools/renpy"
|
||||
BUILDS_DIR="/opt/novelas/builds"
|
||||
WWW_ROOT="/var/www/novelas"
|
||||
WWW_GAMES="$WWW_ROOT/games"
|
||||
NGINX_SNIPPETS_DIR="/etc/nginx/snippets/novelas-games"
|
||||
META_FILE="$PROJECT/meta.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Validaciones basicas
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -z "$NAME" ] || [ -z "$PROJECT" ]; then
|
||||
echo "Uso: $0 <slug> <ruta_proyecto> [renpy|unity|web|onscripter]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PROJECT" ]; then
|
||||
echo "ERROR: No existe el proyecto $PROJECT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Slug seguro: minusculas, numeros, guiones
|
||||
SAFE_NAME=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
if [ -z "$SAFE_NAME" ]; then
|
||||
echo "ERROR: El slug '$NAME' no es valido"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Deteccion / validacion de motor
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -n "$FORCE_ENGINE" ]; then
|
||||
ENGINE="$FORCE_ENGINE"
|
||||
echo "[*] Motor forzado: $ENGINE"
|
||||
else
|
||||
ENGINE=$(/opt/novelas/bin/detect-engine.sh "$PROJECT")
|
||||
echo "[*] Motor detectado: $ENGINE"
|
||||
fi
|
||||
|
||||
case "$ENGINE" in
|
||||
renpy|unity|web|onscripter)
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Motor no soportado: $ENGINE"
|
||||
echo "Estructura no reconocida. Se esperaba:"
|
||||
echo " - renpy: project.json o game/script.rpy"
|
||||
echo " - unity: Build/ + TemplateData/ + index.html"
|
||||
echo " - web: index.html"
|
||||
echo " - onscripter: 0.txt/nscript.dat, ons.cfg o onscripter*.exe"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Validacion especifica por motor
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$ENGINE" = "unity" ]; then
|
||||
if [ ! -d "$PROJECT/Build" ] || [ ! -d "$PROJECT/TemplateData" ] || [ ! -f "$PROJECT/index.html" ]; then
|
||||
echo "ERROR: El proyecto Unity WebGL no tiene la estructura esperada (Build/, TemplateData/, index.html)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ENGINE" = "web" ]; then
|
||||
if [ ! -f "$PROJECT/index.html" ]; then
|
||||
echo "ERROR: El proyecto web no contiene index.html"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ENGINE" = "onscripter" ]; then
|
||||
if [ ! -f "$PROJECT/0.txt" ] && [ ! -f "$PROJECT/nscript.dat" ] && [ ! -f "$PROJECT/ons.cfg" ] && [ ! -f "$PROJECT/onscripter-ru.exe" ] && [ ! -f "$PROJECT/onscripter.exe" ]; then
|
||||
echo "ERROR: El proyecto ONScripter no tiene archivos reconocibles (0.txt, nscript.dat, ons.cfg, onscripter*.exe)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Variables adicionales por motor
|
||||
# -----------------------------------------------------------------------------
|
||||
ONSCRIPTER_WEB_DIR="/opt/novelas/tools/onscripter-yuri/onsyuri_web"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Preparar directorios
|
||||
# -----------------------------------------------------------------------------
|
||||
DEST="$BUILDS_DIR/$SAFE_NAME"
|
||||
rm -rf "$DEST"
|
||||
mkdir -p "$DEST"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Compilar / copiar segun motor
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$ENGINE" = "renpy" ]; then
|
||||
echo "[+] Compilando '$SAFE_NAME' (Ren'Py) desde $PROJECT ..."
|
||||
cd "$RENPY_HOME"
|
||||
# El web_build de Ren'Py ocasionalmente se cuelga al finalizar; usamos timeout
|
||||
timeout 600 xvfb-run -a ./renpy.sh launcher web_build "$PROJECT" --destination "$DEST" || true
|
||||
|
||||
if [ ! -f "$DEST/index.html" ]; then
|
||||
echo "ERROR: No se genero index.html. Revisa el log del proyecto."
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$ENGINE" = "onscripter" ]; then
|
||||
echo "[+] Empaquetando '$SAFE_NAME' (ONScripter) desde $PROJECT ..."
|
||||
cp -r "$PROJECT"/* "$DEST/"
|
||||
|
||||
echo "[+] Copiando motor OnscripterYuri web ..."
|
||||
cp "$ONSCRIPTER_WEB_DIR/onsyuri.js" "$DEST/onsyuri.js"
|
||||
cp "$ONSCRIPTER_WEB_DIR/onsyuri.wasm" "$DEST/onsyuri.wasm"
|
||||
|
||||
echo "[+] Generando onsyuri_index.json ..."
|
||||
python3 "$ONSCRIPTER_WEB_DIR/onsyuri_index.py" \
|
||||
-i "$DEST" \
|
||||
-o "$DEST/onsyuri_index.json" \
|
||||
--title "$SAFE_NAME" \
|
||||
--gamedir "/games/$SAFE_NAME" \
|
||||
--savedir "/onsyuri_save/$SAFE_NAME" \
|
||||
--urlbase "" \
|
||||
--lazyload
|
||||
else
|
||||
echo "[+] Copiando '$SAFE_NAME' ($ENGINE) desde $PROJECT ..."
|
||||
cp -r "$PROJECT"/* "$DEST/"
|
||||
|
||||
if [ ! -f "$DEST/index.html" ]; then
|
||||
echo "ERROR: No se encontro index.html en el proyecto."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Leer metadatos (meta.json opcional) o usar defaults
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f "$META_FILE" ]; then
|
||||
echo "[*] Cargando metadatos desde $META_FILE"
|
||||
TITLE=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('title','$SAFE_NAME'))")
|
||||
SUBTITLE=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('subtitle',''))")
|
||||
DESCRIPTION=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('description',''))")
|
||||
AUTHOR=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('author',''))")
|
||||
VERSION=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('version','1.0'))")
|
||||
COVER=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('cover',''))")
|
||||
else
|
||||
TITLE="$SAFE_NAME"
|
||||
SUBTITLE=""
|
||||
DESCRIPTION=""
|
||||
AUTHOR=""
|
||||
VERSION="1.0"
|
||||
COVER=""
|
||||
fi
|
||||
|
||||
# Cover por defecto segun motor / disponibilidad
|
||||
if [ -z "$COVER" ]; then
|
||||
if [ "$ENGINE" = "renpy" ] && [ -f "$DEST/web-presplash.jpg" ]; then
|
||||
COVER="/games/$SAFE_NAME/web-presplash.jpg"
|
||||
elif [ -f "$DEST/cover.jpg" ]; then
|
||||
COVER="/games/$SAFE_NAME/cover.jpg"
|
||||
elif [ -f "$DEST/cover.png" ]; then
|
||||
COVER="/games/$SAFE_NAME/cover.png"
|
||||
else
|
||||
COVER=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generar index.html para ONScripter tras tener metadatos reales
|
||||
if [ "$ENGINE" = "onscripter" ]; then
|
||||
echo "[+] Adaptando onsyuri.html como index.html ..."
|
||||
sed -e "s|<title>[^<]*</title>|<title>$(echo "$TITLE" | sed 's/[&/\\]/\\&/g')</title>|" \
|
||||
"$ONSCRIPTER_WEB_DIR/onsyuri.html" > "$DEST/index.html"
|
||||
|
||||
if [ ! -f "$DEST/index.html" ]; then
|
||||
echo "ERROR: No se genero index.html para ONScripter."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determinar si requiere headers COOP/COEP (solo Ren'Py Web los necesita)
|
||||
if [ "$ENGINE" = "renpy" ]; then
|
||||
COOP_COEP="true"
|
||||
else
|
||||
COOP_COEP="false"
|
||||
fi
|
||||
|
||||
CREATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Generar game.json
|
||||
# -----------------------------------------------------------------------------
|
||||
cat > "$DEST/game.json" <<EOF
|
||||
{
|
||||
"slug": "$SAFE_NAME",
|
||||
"title": "$TITLE",
|
||||
"subtitle": "$SUBTITLE",
|
||||
"engine": "$ENGINE",
|
||||
"description": "$DESCRIPTION",
|
||||
"cover": "$COVER",
|
||||
"version": "$VERSION",
|
||||
"author": "$AUTHOR",
|
||||
"createdAt": "$CREATED_AT",
|
||||
"entryPoint": "/games/$SAFE_NAME/index.html",
|
||||
"coopCoep": $COOP_COEP
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[+] game.json generado"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Publicar en /var/www/novelas/games/<slug>
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "[+] Publicando en $WWW_GAMES/$SAFE_NAME ..."
|
||||
rm -rf "$WWW_GAMES/$SAFE_NAME"
|
||||
mkdir -p "$WWW_GAMES/$SAFE_NAME"
|
||||
cp -r "$DEST"/* "$WWW_GAMES/$SAFE_NAME/"
|
||||
chown -R www-data:www-data "$WWW_GAMES/$SAFE_NAME"
|
||||
chmod -R 755 "$WWW_GAMES/$SAFE_NAME"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Generar snippet nginx para Ren'Py
|
||||
# -----------------------------------------------------------------------------
|
||||
mkdir -p "$NGINX_SNIPPETS_DIR"
|
||||
SNIPPET_FILE="$NGINX_SNIPPETS_DIR/$SAFE_NAME.conf"
|
||||
|
||||
if [ "$COOP_COEP" = "true" ]; then
|
||||
sed "s/__SLUG__/$SAFE_NAME/g" /opt/novelas/config/nginx-snippet.template > "$SNIPPET_FILE"
|
||||
echo "[+] Snippet nginx generado: $SNIPPET_FILE"
|
||||
else
|
||||
rm -f "$SNIPPET_FILE"
|
||||
echo "[*] No se requiere snippet COOP/COEP para $ENGINE"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Actualizar catalogo global /var/www/novelas/games.json
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "[+] Actualizando catalogo global ..."
|
||||
python3 <<PYEOF
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
catalog_path = "$WWW_ROOT/games.json"
|
||||
games = {}
|
||||
|
||||
if os.path.exists(catalog_path):
|
||||
try:
|
||||
with open(catalog_path, "r", encoding="utf-8") as f:
|
||||
games = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"[!] Advertencia: no se pudo leer catalogo existente: {e}")
|
||||
games = {}
|
||||
|
||||
if "games" not in games or not isinstance(games["games"], list):
|
||||
games["games"] = []
|
||||
|
||||
# Eliminar entrada previa del mismo slug
|
||||
games["games"] = [g for g in games["games"] if g.get("slug") != "$SAFE_NAME"]
|
||||
|
||||
# Cargar metadatos del juego recien publicado
|
||||
with open("$WWW_GAMES/$SAFE_NAME/game.json", "r", encoding="utf-8") as f:
|
||||
game_meta = json.load(f)
|
||||
|
||||
games["games"].append(game_meta)
|
||||
games["games"].sort(key=lambda g: g.get("title", g.get("slug", "")).lower())
|
||||
games["updatedAt"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
games["version"] = "1.0"
|
||||
|
||||
with open(catalog_path, "w", encoding="utf-8") as f:
|
||||
json.dump(games, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"[+] Catalogo actualizado: {len(games['games'])} juego(s)")
|
||||
PYEOF
|
||||
|
||||
chown www-data:www-data "$WWW_ROOT/games.json"
|
||||
chmod 644 "$WWW_ROOT/games.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Recargar nginx
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "[+] Recargando nginx ..."
|
||||
nginx -t && nginx -s reload
|
||||
|
||||
echo "[+] Listo: http://<ip>/games/$SAFE_NAME/"
|
||||
38
bin/detect-engine.sh
Executable file
38
bin/detect-engine.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# detect-engine.sh - Detecta el motor de una novela visual según su estructura
|
||||
# Uso: detect-engine.sh <ruta_proyecto>
|
||||
# Salida: renpy | unity | web | onscripter | unknown
|
||||
|
||||
PROJECT="${1:-}"
|
||||
|
||||
if [ -z "$PROJECT" ] || [ ! -d "$PROJECT" ]; then
|
||||
echo "unknown"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ren'Py: tiene project.json o game/script.rpy
|
||||
if [ -f "$PROJECT/project.json" ] || [ -f "$PROJECT/game/script.rpy" ]; then
|
||||
echo "renpy"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ONScripter: script 0.txt/nscript.dat, ons.cfg o ejecutable onscripter
|
||||
if [ -f "$PROJECT/0.txt" ] || [ -f "$PROJECT/nscript.dat" ] || [ -f "$PROJECT/ons.cfg" ] || [ -f "$PROJECT/onscripter-ru.exe" ] || [ -f "$PROJECT/onscripter.exe" ]; then
|
||||
echo "onscripter"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Unity WebGL: estructura típica
|
||||
if [ -d "$PROJECT/Build" ] && [ -d "$PROJECT/TemplateData" ] && [ -f "$PROJECT/index.html" ]; then
|
||||
echo "unity"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Web genérico: solo necesita index.html
|
||||
if [ -f "$PROJECT/index.html" ]; then
|
||||
echo "web"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "unknown"
|
||||
exit 1
|
||||
44
bin/umineko-web.sh
Executable file
44
bin/umineko-web.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# umineko-web.sh - Gestiona el contenedor Docker de Umineko Web
|
||||
# Uso: umineko-web.sh [start|stop|restart|status|logs|update]
|
||||
|
||||
set -e
|
||||
|
||||
COMPOSE_DIR="/opt/novelas/tools/umineko-web-asm"
|
||||
CONTAINER_NAME="umineko-web-asm_umineko-web_1"
|
||||
|
||||
cd "$COMPOSE_DIR"
|
||||
|
||||
case "${1:-start}" in
|
||||
start|up)
|
||||
echo "[+] Levantando Umineko Web..."
|
||||
./run-umineko-web.sh
|
||||
;;
|
||||
stop|down)
|
||||
echo "[+] Deteniendo Umineko Web..."
|
||||
docker-compose down
|
||||
;;
|
||||
restart)
|
||||
echo "[+] Reiniciando Umineko Web..."
|
||||
docker-compose down
|
||||
./run-umineko-web.sh
|
||||
;;
|
||||
status)
|
||||
echo "[*] Estado del contenedor:"
|
||||
docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
;;
|
||||
logs)
|
||||
echo "[*] Logs del contenedor (Ctrl+C para salir):"
|
||||
docker logs -f "$CONTAINER_NAME"
|
||||
;;
|
||||
update)
|
||||
echo "[+] Actualizando imagen y reiniciando..."
|
||||
docker-compose down
|
||||
docker-compose pull
|
||||
./run-umineko-web.sh
|
||||
;;
|
||||
*)
|
||||
echo "Uso: $0 [start|stop|restart|status|logs|update]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user