diff --git a/.forgejo/workflows/build-on-tag.yml b/.forgejo/workflows/build-on-tag.yml index 888102b..e7607c9 100644 --- a/.forgejo/workflows/build-on-tag.yml +++ b/.forgejo/workflows/build-on-tag.yml @@ -29,9 +29,8 @@ jobs: # Build and push multi-platform Docker images docker build -t $REPO_HOST/$REPO_PATH:$TAG --push . # Tag and push latest - docker tag $REPO_HOST/$REPO_PATH:$TAG $REPO_HOST/$REPO_PATH:latest - docker push $REPO_HOST/$REPO_PATH:latest + docker tag $REPO_HOST/$REPO_PATH:$TAG + docker push $REPO_HOST/$REPO_PATH:$TAG # Remove the local image to save storage - docker rmi $REPO_HOST/$REPO_PATH:$TAG - docker rmi $REPO_HOST/$REPO_PATH:latest \ No newline at end of file + docker rmi $REPO_HOST/$REPO_PATH:$TAG \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index afe92f8..36b362c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,10 @@ -FROM mcr.microsoft.com/dotnet/runtime +FROM mcr.microsoft.com/dotnet/sdk AS SDK + +COPY ./defaults/loader /loader +WORKDIR /loader +RUN dotnet build -o /app -c Release + +FROM mcr.microsoft.com/dotnet/runtime AS runtime ENV \ LANG="en_US.UTF-8" \ @@ -21,6 +27,7 @@ RUN DEBIAN_FRONTEND=noninteractive \ locales \ mono-complete \ opus-tools \ + unzip \ sudo # make data directories @@ -38,6 +45,7 @@ ENV STOP_LAUNCH=false COPY defaults /mnt/defaults COPY scripts /scripts +COPY --from=SDK /app/ /app/ RUN chmod +x /scripts/* ENTRYPOINT ["/scripts/00_setup.sh"] diff --git a/Readme.md b/Readme.md index 2e110c4..2904b26 100644 --- a/Readme.md +++ b/Readme.md @@ -1,93 +1,35 @@ - -# Resonite Headless Docker Image & CI Workflows +# README for Docker Image Build and Publish Workflows ## Overview -This repository provides a Docker image and automation scripts for running [Resonite Headless](https://resonite.com/) in a containerized environment. It includes: +This repository contains two GitHub Actions workflows that automate the building and publishing of Docker images to an OCI registry. -- Dockerfile and Docker Compose setup -- Scripts for installation, configuration, and mod management -- CI workflows for automated image builds and publishing +### Workflows +1. **On Commit to Main** + - **Trigger:** Activates on commits to the `main` branch (tags are excluded). + - **Purpose:** Builds and publishes a Docker image for each commit. -## Quick Start +2. **On Tag Push** + - **Trigger:** Activates when a new tag is pushed. + - **Purpose:** Builds and publishes a Docker image for the tag and tags it as `latest`. -1. **Create or edit your `docker-compose.yml`:** +## Prerequisites - Example: - ```yaml - services: - resonite: - image: git.merith.xyz/oci/resonite:nightly - container_name: resonite - network_mode: host # dont actually know what ports resonite needs - stdin_open: true - tty: true - volumes: - - ./data:/data:rw - - ./data/resonite:/data/resonite:rw - - ./data/steamcmd:/data/steamcmd:rw - - ./scripts:/scripts:ro ## Optional if you need an override for one of the scripts - environment: - DISABLE_STEAMCMD: false - CONFIG_FILE: "/data/Config.json" - COMMAND: "/scripts/99_start.sh" - RESONITE_MOD_LOADER: true - MOD_URLS: | - https://github.com/Raidriar796/StresslessHeadless/releases/download/2.0.0-rc1/StresslessHeadless.dll - RUN_AS: 1000 - STOP_LAUNCH: false - env_file: - - ./steamcred.env - ``` +- **Secrets Needed:** + - `OCI_TOKEN`: Your OCI registry token. + - `OCI_USER`: Your OCI registry username. -2. **Configure environment variables:** - - Copy `steamcred.env` and fill in your Steam credentials (`STEAM_USER`, `STEAM_PASS`, `STEAM_AUTH`, `STEAM_BETA`). - - Edit `docker-compose.yml` to adjust options as needed (see above). +## How to Use -3. **Start the container:** - ```sh - docker-compose up -d - ``` +1. **Clone the Repository:** Get a local copy of this repository. +2. **Modify Dockerfile:** Update the `Dockerfile` for your application. +3. **Push Changes:** Push changes to the `main` branch or create a new tag. +4. **Check Workflow Status:** View the Actions tab in Forgjo to monitor workflow runs. -## Quick Reference +## Notes -- **Docker image:** `git.merith.xyz/oci/resonite:nightly` -- **Repository:** https://git.merith.xyz/oci/resonite -- **Default compose file:** See example above or use the provided `docker-compose.yml`. - -## Configuration - -### Environment Variables (in `docker-compose.yml`) - -- `DISABLE_STEAMCMD`: Prevents SteamCMD from updating game files (default: false) -- `CONFIG_FILE`: Path to config file (default: `/data/Config.json`) -- `COMMAND`: Startup script (default: `/scripts/99_start.sh`) -- `RESONITE_MOD_LOADER`: Enable Resonite Mod Loader (default: false) -- `MOD_URLS`: List of mod URLs to download -- `RUN_AS`: UID:GID to run the server as (default: root) -- `STOP_LAUNCH`: If true, prevents server from starting (for debugging) - -### Steam Credentials -Set in `steamcred.env` (not tracked in git): -- `STEAM_USER`, `STEAM_PASS`, `STEAM_AUTH`, `STEAM_BETA` - -### Volumes -- `/data`: Persistent data -- `/data/resonite`: Game files -- `/data/steamcmd`: SteamCMD files - -## Scripts - -- `00_setup.sh`: Main entrypoint, sets up environment and runs install/config/mod scripts -- `01_install.sh`: Installs Resonite Headless via SteamCMD -- `02_setup_config.sh`: Sets up configuration file from template if missing -- `03_download_mods.sh`: Downloads mods and sets up mod loader if enabled -- `99_start.sh`: Starts the Resonite server - -## Mod Support - -To enable mod support, set `RESONITE_MOD_LOADER: true` and provide mod URLs in `MOD_URLS`. The scripts will download and link mods automatically. +- Ensure your Docker environment is compatible with multi-platform builds if necessary. ## License diff --git a/defaults/loader/Loader.cs b/defaults/loader/Loader.cs new file mode 100644 index 0000000..38d8876 --- /dev/null +++ b/defaults/loader/Loader.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +class Program +{ + static void Main(string[] args) + { + // Hardcoded working directory + string workDir = "/data/resonite/Headless"; + string resonitePath = Path.Combine(workDir, "Resonite.dll"); + string winhttpPath = Path.Combine(workDir, "winhttp.dll"); + + Console.WriteLine($"Using hardcoded work directory: {workDir}"); + + // Verify work directory exists + if (!Directory.Exists(workDir)) + { + Console.WriteLine($"Error: Work directory '{workDir}' does not exist."); + return; + } + + // Verify winhttp.dll exists + if (!File.Exists(winhttpPath)) + { + Console.WriteLine($"Error: Cannot find winhttp.dll at {winhttpPath}"); + return; + } + + // Load the winhttp.dll + IntPtr handle = LoadLibrary(winhttpPath); + if (handle == IntPtr.Zero) + { + Console.WriteLine("Failed to load winhttp.dll"); + return; + } + + Console.WriteLine("winhttp.dll loaded successfully"); + + // Verify Resonite.dll exists + if (!File.Exists(resonitePath)) + { + Console.WriteLine($"Error: Cannot find Resonite.dll at {resonitePath}"); + return; + } + + // Load and execute Resonite.dll + Assembly resoniteAssembly = Assembly.LoadFrom(resonitePath); + + // Find the entry point and invoke it + MethodInfo entryPoint = resoniteAssembly.EntryPoint; + if (entryPoint == null) + { + Console.WriteLine("Error: Could not find entry point in Resonite.dll"); + return; + } + + Console.WriteLine("Executing Resonite.dll with provided arguments..."); + entryPoint.Invoke(null, new object[] { args }); + } + + #if WINDOWS + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibrary(string lpFileName); + #else + [DllImport("libc.so.6", SetLastError = true)] + private static extern IntPtr dlopen(string fileName, int flags); + + private static IntPtr LoadLibrary(string fileName) + { + const int RTLD_NOW = 2; + return dlopen(fileName, RTLD_NOW); + } + #endif +} diff --git a/defaults/loader/Loader.csproj b/defaults/loader/Loader.csproj new file mode 100644 index 0000000..1035c2d --- /dev/null +++ b/defaults/loader/Loader.csproj @@ -0,0 +1,11 @@ + + + + Exe + net9.0 + enable + enable + true + + + diff --git a/docker-compose.yml b/docker-compose.yml index b97e11f..98088e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,7 @@ services: resonite: - # build: . - image: git.merith.xyz/oci/resonite:nightly - - container_name: resonite + build: . + # image: git.merith.xyz/oci/resonite:nightly network_mode: host stdin_open: true tty: true @@ -25,7 +23,7 @@ services: ## These have their defualts set in the Dockerfile, and are safe to remove from this file # Prevents SteamCMD from updating the gamefiles - DISABLE_STEAMCMD: false + DISABLE_STEAMCMD: "false" # Where to located the config file, defaults to /data/Config.json # if config file does not exist, it will generate a template one at that location @@ -35,11 +33,12 @@ services: COMMAND: "/scripts/99_start.sh" # Wether to enable the Resonite Mod Loader - RESONITE_MOD_LOADER: true + MONKEY_LOADER: false # list of mods to load, MOD_URLS: | https://github.com/Raidriar796/StresslessHeadless/releases/download/2.0.0-rc1/StresslessHeadless.dll + # https://github.com/New-Project-Final-Final-WIP/HeadlessTweaks/releases/latest/download/HeadlessTweaks.dll # Controls the UID:GID of the headless, defaults to `root` if not set diff --git a/scripts/02_setup_config.sh b/scripts/02_setup_config.sh index 5ec5d7c..30de30e 100755 --- a/scripts/02_setup_config.sh +++ b/scripts/02_setup_config.sh @@ -1,10 +1,10 @@ #!/bin/bash ## Setup Resonite Config -echo "Setting up Resonite Config" +echo "Setting up Resonite Config" # if CONFIG_FILE is not set, use default config path -if [ ! -f $CONFIG_FILE ]; then +if [ ! -f $CONFIG_FILE ]; then echo "No Resonite Config found, copying from template" cp /mnt/defaults/resonite.json $CONFIG_FILE fi @@ -16,10 +16,54 @@ if [ ! -n "$CONFIG_DATA" ]; then CONFIG_DATA=$(grep -v " null," "$CONFIG_FILE") fi -## Setup Modloader Configs -if [ "$RESONITE_MOD_LOADER" == "true" ]; then - echo "Setting up Modloader Configs" - DEFAULT_RESONITE_ARGS=$(echo "$DEFAULT_RESONITE_ARGS -LoadAssembly /data/resonite/Headless/Libraries/ResoniteModLoader.dll") +if [ "$MONKEY_LOADER" == "true" ]; then + echo "Downloading MonkeyLoader" + + # Define variables + monkeyloader_url="https://github.com/ResoniteModdingGroup/MonkeyLoader.GamePacks.Resonite/releases/download/v0.21.2/MonkeyLoader-v0.24.0+Resonite-v0.21.2+RML-v3.0.4.zip" + destination_dir="/data/resonite/Headless" + zip_file="$destination_dir/MonkeyLoader.zip" + + # Check if the zip file already exists + if [ ! -f "$zip_file" ]; then + echo "Downloading from $monkeyloader_url to $zip_file" + curl -L -o "$zip_file" "$monkeyloader_url" + else + echo "File already exists: $zip_file, skipping download." + fi + + if [ ! -f "/data/resonite/Headless/run_monkeyloader.sh" ]; then + # Extract the zip file + echo "Extracting $zip_file to $destination_dir" + unzip -o "$zip_file" -d "$destination_dir" + + # Clean up zip-specific structure (move contents correctly if necessary) + if [ -d "$destination_dir/zipfiles" ]; then + echo "Moving extracted contents to $destination_dir" + mv "$destination_dir/zipfiles/"* "$destination_dir" + rmdir "$destination_dir/zipfiles" + fi + fi + + if [ -f "$destination_dir/MonkeyLoader/GamePacks/MonkeyLoader.GamePacks.Resonite.Unity.nupkg" ]; then + rm "$destination_dir/MonkeyLoader/GamePacks/MonkeyLoader.GamePacks.Resonite.Unity.nupkg" + fi + if [ -f "$destination_dir/MonkeyLoader/GamePacks/MonkeyLoader.GamePacks.Unity.nupkg" ]; then + rm "$destination_dir/MonkeyLoader/GamePacks/MonkeyLoader.GamePacks.Unity.nupkg" + fi + + echo "MonkeyLoader setup complete." else - echo "Modloader is disabled" -fi \ No newline at end of file + echo "MonkeyLoader Disabled" + + # Remove MonkeyLoader directory and run_monkeyloader.sh script if they exist + if [ -d "/data/resonite/Headless/MonkeyLoader" ]; then + echo "Removing MonkeyLoader directory" + rm -rf "/data/resonite/Headless/MonkeyLoader" + fi + + if [ -f "/data/resonite/Headless/run_monkeyloader.sh" ]; then + echo "Removing run_monkeyloader.sh script" + rm -f "/data/resonite/Headless/run_monkeyloader.sh" + fi +fi diff --git a/scripts/03_download_mods.sh b/scripts/03_download_mods.sh index 9087bf3..be78cc6 100755 --- a/scripts/03_download_mods.sh +++ b/scripts/03_download_mods.sh @@ -7,14 +7,6 @@ # want to use simply because google made it... # shell scripts are typically known by docker hosters so... -# Define file URLs and their associated positions -file_urls=( - "https://github.com/resonite-modding-group/ResoniteModLoader/releases/latest/download/ResoniteModLoader.dll /data/resonite/Headless/Libraries/ResoniteModLoader.dll" - # "https://github.com/resonite-modding-group/ResoniteModLoader/releases/latest/download/0Harmony.dll /data/resonite/Headless/rml_libs/0Harmony.dll" - # net9 Harmony - "https://github.com/stiefeljackal/pardeike.Harmony/releases/download/temp-release/0Harmony.dll /data/resonite/Headless/rml_libs/0Harmony.dll" -) - # Function to download a file from URL to destination download_file() { local url="$1" @@ -30,30 +22,33 @@ download_file() { fi } -# Loop through each file URL and download -for file_url in "${file_urls[@]}"; do - read -r url destination <<< "$file_url" - # Backup existing file if exists - if [ -f "$destination" ]; then - mv "$destination" "${destination}.bak" - fi - # Download file - download_file "$url" "$destination" -done - # Download additional files from a list of URLs to /data/resonite/Headless/rml_mods # shellcheck disable=SC2153 -for url in $MOD_URLS ; do - destination="/data/resonite/Headless/rml_mods/$(basename "$url")" +for url in $MOD_URLS; do + modname=$(basename "$url") + destination="" + + # Determine the destination based on the file extension + if [[ "$modname" == *.dll ]]; then + destination="/data/resonite/Headless/rml_mods/$modname" + elif [[ "$modname" == *.nupkg ]]; then + destination="/data/resonite/Headless/MonkeyLoader/Mods/$modname" + else + echo "Unknown file type for $modname, skipping." + continue + fi + # Check if file already exists, if yes, skip download if [ ! -f "$destination" ]; then echo "Downloading mod: $url" download_file "$url" "$destination" + else + echo "File already exists: $destination, skipping download." fi done # if resonte mod loader is enabled, create and link rml_mods, libs, and config -if [ "$RESONITE_MOD_LOADER" == "true" ]; then +if [ "$MONKEY_LOADER" == "true" ]; then for dir in rml_mods rml_libs rml_config; do if [ -d "/data/resonite/Headless/$dir" ]; then continue @@ -61,8 +56,4 @@ if [ "$RESONITE_MOD_LOADER" == "true" ]; then mkdir -p "/data/resonite/Headless/$dir" ln -s "./Headless/$dir" "./$dir" done - if [ ! -f "/data/resonite/Libraries/ResoniteModLoader.dll" ]; then - mkdir -p "/data/resonite/Libraries" - ln -s "/data/resonite/Headless/Libraries/ResoniteModLoader.dll" "/data/resonite/Libraries/ResoniteModLoader.dll" - fi fi \ No newline at end of file diff --git a/scripts/99_start.sh b/scripts/99_start.sh index a7eeab5..238fe84 100755 --- a/scripts/99_start.sh +++ b/scripts/99_start.sh @@ -1,11 +1,21 @@ #!/bin/bash +cd /data/resonite/Headless || exit + if [ "$STOP_LAUNCH" == "true" ]; then echo "STOP_LAUNCH is set to true, exiting..." exit 0 fi -cd /data/resonite/Headless || exit + echo "Running Resonite" -echo "exec: dotnet Resonite.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS" -dotnet Resonite.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS +if [ "$MONKEY_LOADER" == "true" ]; then + echo "exec: dotnet /app/Loader.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS" + # dotnet Resonite.dll -LoadAssembly MonkeyLoader/MonkeyLoader.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS + # bash run_monkeyloader.sh $DEFAULT_RESONITE_ARGS $RESONITE_ARGS + dotnet /app/Loader.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS + +else + echo "exec: dotnet Resonite.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS" + dotnet Resonite.dll $DEFAULT_RESONITE_ARGS $RESONITE_ARGS +fi \ No newline at end of file