Initial commit: NovelasVM platform with multi-engine support and Umineko Web integration

This commit is contained in:
2026-06-14 23:51:40 +00:00
commit 8ded9cc4c8
24 changed files with 3688 additions and 0 deletions

290
bin/build-novela.sh Executable file
View 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
View 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
View 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