Files
romm/entrypoint.sh
T
Georges-Antoine AssiandClaude Opus 5.5 6f2a361481 Play RPG Maker 2000/2003 games in the browser with EasyRPG (#5077)
* feat(player): play RPG Maker 2000/2003 games in the browser with EasyRPG

Embed the EasyRPG web player for extracted game folders on the rpg-maker
platform. The backend generates the player's index.json from the scanned
files and merges in the free EasyRPG RTP under every name the official
and fan RTPs give its assets, so games that lean on the RTP find them.
In-game saves sync with the user's RomM saves through the player's
IndexedDB storage.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): read the free RTP from a fixed path instead of an env var

The full image always keeps the RTP at /var/www/html/assets/easyrpg/rtp,
so the path is a constant beside the web path nginx serves it under. The
dev container links its copy there at startup.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(easyrpg): code-review fixes for the save sync and asset endpoint

- Wait for an in-flight push before the quit push, so the newest save uploads
- Keep Play disabled until the fresh rom (and its saves) is fetched
- Stop onPlay and the rom fetch from acting after the view unmounts
- Leave through usePlayerExit like the other players
- Handle a rejected IDB commit when the run itself fails
- Track uploads by the browser's file name
- Load only visibility and files per asset request, not the full rom
- Serve DEV_MODE files as octet-stream, matching nginx
- Make the traversal test actually reach the endpoint

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): simplify pass over the player and asset endpoint

- Load only file paths per asset request, and look up RTP folders in O(1)
- Reuse RomFile.file_name_for_download and usePlayerFullscreen
- Share one dev/prod serve helper for game and RTP files
- Derive the save sync's game key from the rom
- Have the RTP table tool reuse normalize_name
- Make save-module internals private and tidy the prepare/read loops

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(easyrpg): flag RPG Maker game folders on the rom schemas

Gallery lists carry no files, so the frontend guessed that every multi-file
rom on the rpg-maker platform was playable, including RPG Maker XP/VX/MV
folders. Rom now has a deferred `is_easyrpg_game` subquery (an RPG_RT.ldb at
the folder root), loaded with the other gallery file stats and derived from
loaded files on the with_files path, and the frontend reads it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): drop server save sync, keep saves per user in the browser

Server sync moves to a follow-up that puts the web players on device sync.
Saves now stay in the player's IndexedDB, as js-dos does, with the same note
on the start page. The player is launched as `<rom id>-<user id>` so each
RomM account sharing a browser profile keeps its own saves; nginx and Vite
drop the user id when routing to the backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(easyrpg): address Greptile review

- Start timing the play session once the player page loads
- Use object-form props in the view test's button mock
- Keep the RTP table tool's docstring to one sentence

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): keep the RTP table in a fixtures folder

Matches handler/metadata/fixtures, where the other bundled data files live.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-03 17:51:38 -04:00

153 lines
4.9 KiB
Bash

#!/bin/bash
set -e
echo "Starting entrypoint script..."
# Create symlinks for frontend
if [[ -L /app/frontend/assets/romm/resources ]]; then
target=$(readlink "/app/frontend/assets/romm/resources")
# If the target is not the same as ${ROMM_BASE_PATH}/resources, recreate the symbolic link.
if [[ ${target} != "${ROMM_BASE_PATH}/resources" ]]; then
rm "/app/frontend/assets/romm/resources"
ln -s "${ROMM_BASE_PATH}/resources" "/app/frontend/assets/romm/resources"
fi
elif [[ ! -e /app/frontend/assets/romm/resources ]]; then
# Ensure parent directory exists before creating symbolic link
mkdir -p "/app/frontend/assets/romm"
ln -s "${ROMM_BASE_PATH}/resources" "/app/frontend/assets/romm/resources"
fi
# The ./frontend bind mount hides whatever the build wrote under /app/frontend,
# so link the image's emulator runtimes into the tree the dev server serves.
if [[ -n ${EMULATOR_ASSETS_DIR-} && -d ${EMULATOR_ASSETS_DIR} ]]; then
for runtime_dir in "${EMULATOR_ASSETS_DIR}"/*; do
target="/app/frontend/assets/$(basename "${runtime_dir}")"
mkdir -p "${target}"
for entry in "${runtime_dir}"/*; do
link="${target}/$(basename "${entry}")"
# Never replace checked-in art that shares a name with a runtime file.
if [[ -e ${link} && ! -L ${link} ]]; then
continue
fi
ln -sfn "${entry}" "${link}"
done
done
fi
# The backend reads the free RTP where the full image keeps it.
if [[ -n ${EMULATOR_ASSETS_DIR-} && -d ${EMULATOR_ASSETS_DIR}/easyrpg/rtp ]]; then
mkdir -p /var/www/html/assets/easyrpg
ln -sfn "${EMULATOR_ASSETS_DIR}/easyrpg/rtp" /var/www/html/assets/easyrpg/rtp
fi
# Define a signal handler to propagate termination signals
function handle_termination() {
echo "Terminating child processes..."
# Kill all background jobs
# trunk-ignore(shellcheck)
kill -TERM $(jobs -p) 2>/dev/null
}
# Trap SIGTERM and SIGINT signals
trap handle_termination SIGTERM SIGINT
# Set ROMM_AUTH_SECRET_KEY if not already set
if [[ -z ${ROMM_AUTH_SECRET_KEY-} ]]; then
ROMM_AUTH_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
export ROMM_AUTH_SECRET_KEY
fi
# Start all services in the background
echo "Starting backend..."
cd /app/backend
if [[ ${DEV_MODE:-false} == "true" ]]; then
echo "Starting backend under debugpy on :5678..."
# Add --wait-for-client after --listen to pause until VSCode attaches.
uv run python -m debugpy --listen 0.0.0.0:5678 main.py &
else
uv run python main.py &
fi
# REDIS_SSL is a boolean to the app, so "false" and "0" mean plaintext.
REDIS_USERINFO=""
REDIS_SSL_VALUE="${REDIS_SSL-}"
case "${REDIS_SSL_VALUE,,}" in
1 | true | yes | on) REDIS_SCHEME="rediss" ;;
*) REDIS_SCHEME="redis" ;;
esac
if [[ -n ${REDIS_PASSWORD-} ]]; then
REDIS_USERINFO="${REDIS_USERNAME-}:${REDIS_PASSWORD}@"
elif [[ -n ${REDIS_USERNAME-} ]]; then
REDIS_USERINFO="${REDIS_USERNAME}@"
fi
REDIS_URL="${REDIS_SCHEME}://${REDIS_USERINFO}${REDIS_HOST:-127.0.0.1}:${REDIS_PORT:-6379}/${REDIS_DB:-0}"
echo "Starting RQ cron scheduler..."
# The URL carries the password, so it goes through RQ_REDIS_URL rather than
# --url, which would put it on a world-readable command line.
PYTHONPATH="/app/backend:${PYTHONPATH-}" \
RQ_REDIS_URL="${REDIS_URL}" \
rq cron \
--path /app/backend \
--logging-level "${LOGLEVEL:-INFO}" \
tasks.cron_config &
# Set PYTHONPATH so RQ can find the tasks module.
# Use a worker class that drops the noisy per-sweep "cleaning registries for
# queue" log line. The maintenance interval keeps its default (~10 min) so
# orphaned STARTED jobs and stale workers are still pruned promptly.
# --with-scheduler releases delayed jobs, which is how the watcher's rescans
# wait out their delay.
start_rq_worker() {
local name="$1"
local worker_class="$2"
shift 2
PYTHONPATH="/app/backend:${PYTHONPATH-}" \
RQ_REDIS_URL="${REDIS_URL}" \
rq worker \
--path /app/backend \
--worker-class "handler.rq_worker.${worker_class}" \
--pid "/tmp/${name}.pid" \
--logging_level "${LOGLEVEL:-INFO}" \
--with-scheduler \
"$@" &
}
echo "Starting RQ worker..."
start_rq_worker rq_worker RomMWorker high default low
# Scans get a worker of their own, see SCAN_QUEUE_NAME.
echo "Starting RQ scan worker..."
start_rq_worker rq_scan_worker RomMWorker scans
# Streaming teardowns get a worker of their own, see STREAMING_QUEUE_NAME. It
# preloads the reaper, which runs every minute when streaming is on.
echo "Starting RQ streaming worker..."
start_rq_worker rq_streaming_worker PreloadingWorker streaming
echo "Starting watcher..."
watchfiles \
--target-type command \
'uv run python watcher.py' \
/app/romm/library &
if [[ ${ENABLE_SYNC_FOLDER_WATCHER:-false} == "true" ]]; then
echo "Starting sync folder watcher..."
sync_base_path="${ROMM_BASE_PATH:-/romm}/sync"
mkdir -p "${sync_base_path}"
watchfiles \
--target-type command \
'uv run python sync_watcher.py' \
"${sync_base_path}" &
fi
# Start the frontend dev server
cd /app/frontend
npm run dev &
# Wait for all background processes
wait