Initial commit

This commit is contained in:
Blacky
2025-06-09 23:07:47 +02:00
commit 3f17bab5a9
641 changed files with 2917 additions and 0 deletions

15
.gitattributes vendored Normal file
View File

@@ -0,0 +1,15 @@
*.db filter=lfs diff=lfs merge=lfs -text
*.exe filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.PNG filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.osk filter=lfs diff=lfs merge=lfs -text
*.osr filter=lfs diff=lfs merge=lfs -text
*.osz filter=lfs diff=lfs merge=lfs -text
*.pack filter=lfs diff=lfs merge=lfs -text
*.pdn filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.WAV filter=lfs diff=lfs merge=lfs -text

860
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,860 @@
name: CI/CD Pipeline
on:
push:
branches:
- main
paths:
- '.gitea/workflows/*'
- 'Skins/**/*'
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuild all skins'
required: false
default: 'false'
env:
DANSER_PATH: "/app/danser/danser-cli"
DANSER_DIR: "/app/danser"
DANSER_VIDEO_DIR: "/app/danser/videos"
DANSER_SCREENSHOT_DIR: "/app/danser/screenshots"
SKINS_DIR: "${{ github.workspace }}/Skins"
DANSER_SKINS_DIR: "/app/danser/skins"
DEFAULT_SKIN_DIR: "${{ github.workspace }}/src/default-skin"
REPO_SCREENSHOT_DIR: "${{ github.workspace }}/media/gameplay"
REPO_MOD_ICONS_DIR: "${{ github.workspace }}/media/icons"
REPO_RANKING_PANEL_DIR: "${{ github.workspace }}/media/panel"
SETTINGS_JSON_PATH: "/app/danser/settings/default.json"
README_PATH: "${{ github.workspace }}/README.md"
REPLAY_PATH: "${{ github.workspace }}/src/replay.osr"
OSK_PATH: "${{ github.workspace }}/export"
IMAGE_NAME: osc/skins-image
REGISTRY_URL: "https://${{ vars.CONTAINER_REGISTRY }}"
OSU_ID: ${{ vars.OSUID }}
jobs:
generate_everything:
name: Full CI/CD Pipeline
runs-on: ubuntu-latest
container:
image: ${{ vars.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
options: >-
--gpus all
--privileged
--security-opt seccomp=unconfined
--security-opt apparmor=unconfined
--cap-add=ALL
--env NVIDIA_DRIVER_CAPABILITIES=all
--env NVIDIA_VISIBLE_DEVICES=all
--user 0:0
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
token: ${{ secrets.TOKEN }}
- name: Detect Changed Skin Directories
shell: bash
run: |
echo "[Detect Changed Skin Directories Started]"
echo "→ Fetching tags from git..."
git fetch --tags
force_rebuild="${{ github.event.inputs.force_rebuild }}"
skins=()
echo "→ Force rebuild flag: $force_rebuild"
if [ "$force_rebuild" = "true" ]; then
echo "→ Force rebuild is enabled. Finding all skin directories..."
mapfile -t skins < <(find Skins -mindepth 1 -maxdepth 1 -type d | sed 's|^Skins/||' | sort)
echo " ✓ Found ${#skins[@]} skin directories"
else
echo "→ Force rebuild is disabled. Finding latest git tag..."
latest_tag=$(git tag --sort=-creatordate | head -n 1 || true)
if [ -n "$latest_tag" ]; then
echo "→ Latest tag found: $latest_tag"
echo "→ Finding skins changed since $latest_tag..."
mapfile -t skins < <(
git diff --name-only "$latest_tag" HEAD |
grep '^Skins/' | sed -E 's#^Skins/([^/]+).*#\1#' | sort -u
)
echo " ✓ Found ${#skins[@]} changed skins"
else
echo "→ No tag found. Falling back to finding all skin directories..."
mapfile -t skins < <(find Skins -mindepth 1 -maxdepth 1 -type d | sed 's|^Skins/||' | sort)
echo " ✓ Found ${#skins[@]} skin directories"
fi
fi
echo ""
echo "[Cleaning Skin Names]"
uniq_skins=()
for skin in "${skins[@]}"; do
skin="${skin#"${skin%%[![:space:]]*}"}"
skin="${skin%"${skin##*[![:space:]]}"}"
if [ -n "$skin" ]; then
uniq_skins+=("$skin")
fi
done
echo " ✓ ${#uniq_skins[@]} valid skin names after cleaning"
echo ""
if [ "${#uniq_skins[@]}" -eq 0 ]; then
echo "→ No changed skins detected."
echo "CHANGED_SKINS_FILE=" >> "$GITHUB_ENV"
else
echo "[Writing Changed Skins to File]"
changed_skins_file=$(mktemp)
printf "%s\n" "${uniq_skins[@]}" > "$changed_skins_file"
echo " ✓ Skins written to $changed_skins_file"
echo "CHANGED_SKINS_FILE=$changed_skins_file" >> "$GITHUB_ENV"
fi
echo ""
echo "[Detect Changed Skin Directories Complete — ${#uniq_skins[@]} skins processed]"
- name: Pull Git LFS objects for src and changed skins
shell: bash
run: |
# always include your source files
includes="src/**"
if [[ -n "$CHANGED_SKINS_FILE" ]]; then
skin_includes=$(
while IFS= read -r skin; do
esc=$(printf '%s' "$skin" \
| sed -e 's/\[/\\[/g' -e 's/\]/\\]/g' )
printf 'Skins/%s/**\n' "$esc"
done < "$CHANGED_SKINS_FILE" \
| paste -sd ','
)
includes="$includes,$skin_includes"
fi
echo "→ Pulling LFS objects for patterns: $includes"
git lfs pull --include="$includes"
- name: Extract Repository path
shell: bash
run: |
echo "Extracting repository path..."
USER_REPOSITORY="${{ github.workspace }}"
USER_REPOSITORY="${USER_REPOSITORY#/workspace/}"
USER_REPOSITORY="${USER_REPOSITORY%/}"
echo "Repository path extracted: $USER_REPOSITORY"
echo "USER_REPOSITORY=$USER_REPOSITORY" >> $GITHUB_ENV
- name: Set XDG_RUNTIME_DIR
shell: bash
run: |
echo "Setting XDG_RUNTIME_DIR..."
mkdir -p /tmp/xdg_runtime_dir
chmod 0700 /tmp/xdg_runtime_dir
echo "XDG_RUNTIME_DIR=/tmp/xdg_runtime_dir" >> "$GITHUB_ENV"
echo "XDG_RUNTIME_DIR set."
- name: Create directories for assets
shell: bash
run: |
echo "Creating directories for assets..."
mkdir -p "$REPO_SCREENSHOT_DIR" "$REPO_MOD_ICONS_DIR" "$REPO_RANKING_PANEL_DIR" "$OSK_PATH"
echo "Directories created."
- name: Create New Tag
shell: bash
run: |
echo "Computing new tag..."
git fetch --tags
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null || echo "")
if [ -z "$latest_tag" ]; then
new_tag="v1.0.0"
else
IFS='.' read -r major minor patch <<< "${latest_tag#v}"
minor=$((minor + 1))
patch=0
new_tag="v${major}.${minor}.${patch}"
fi
echo "new_tag=$new_tag" >> $GITHUB_ENV
echo "Computed new tag: $new_tag"
- name: Move Skin files to Danser Skins directory
shell: bash
run: |
echo "Moving Skin files to Danser Skins directory..."
mkdir -p "$DANSER_SKINS_DIR"
mv "$SKINS_DIR"/* "$DANSER_SKINS_DIR"
echo "Skin files moved."
- name: Check for Duplicate skin.ini Names
shell: bash
run: |
set -euo pipefail
echo "[Duplicate Skin Name Check Started]"
# Guard: ensure DANSER_SKINS_DIR is set and exists
if [ -z "${DANSER_SKINS_DIR:-}" ] || [ ! -d "${DANSER_SKINS_DIR}" ]; then
echo "DANSER_SKINS_DIR is not set or not a directory. Exiting."
exit 1
fi
sanitize_filename() {
local filename="$1"
echo "$filename" \
| sed -e 's#[\\/:\*\?"<>|]#-#g' -e 's#%#_#g' \
| tr -s ' ' \
| sed 's/^ *//;s/ *$//'
}
declare -A name_counts=()
declare -A name_dirs=()
duplicates=()
shopt -s nullglob
for dir in "${DANSER_SKINS_DIR}"/*; do
[ -d "$dir" ] || continue
dir_base=$(basename "$dir" | tr -d $'\r\n')
name="$dir_base"
ini=$(find "$dir" -maxdepth 1 -iname "skin.ini" | head -n1 || true)
if [ -f "$ini" ]; then
line=$(grep -i '^[[:space:]]*Name:' "$ini" | head -n1 || true)
if [ -n "${line:-}" ]; then
val="${line#*:}"
val=$(echo "$val" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
name=$(sanitize_filename "$val")
fi
fi
name_counts["$name"]=$(( ${name_counts["$name"]:-0} + 1 ))
if [ -z "${name_dirs[$name]:-}" ]; then
name_dirs["$name"]="$dir_base"
else
name_dirs["$name"]+=",${dir_base}"
fi
done
shopt -u nullglob
for nm in "${!name_counts[@]}"; do
if [ "${name_counts[$nm]}" -gt 1 ]; then
duplicates+=("$nm")
fi
done
if [ "${#duplicates[@]}" -gt 0 ]; then
echo "⚠️ Duplicate skin.ini Names found:"
for nm in "${duplicates[@]}"; do
IFS=',' read -r -a dirs <<< "${name_dirs[$nm]}"
echo " • $nm (${name_counts[$nm]} skins):"
for d in "${dirs[@]}"; do
echo " - $d"
done
done
else
echo "✓ No duplicate skin.ini Names detected."
fi
echo "[Duplicate Skin Name Check Completed]"
- name: Generate Danser videos and screenshots
shell: bash
run: |
echo "[Danser Job Started]"
if [ -z "${CHANGED_SKINS_FILE:-}" ] || [ ! -s "$CHANGED_SKINS_FILE" ]; then
echo "No skins changed. Skipping generation."
exit 0
fi
mapfile -t skins < "$CHANGED_SKINS_FILE"
if [ "${#skins[@]}" -eq 0 ]; then
echo "No skins changed after reading file. Skipping generation."
exit 0
fi
SKIN_COUNT=${#skins[@]}
INDEX=1
for skin_path in "${skins[@]}"; do
[ -z "$skin_path" ] && continue
SKIN_DIR="$DANSER_SKINS_DIR/$skin_path"
if [ ! -d "$SKIN_DIR" ]; then
echo "Skipping missing skin directory: $SKIN_DIR"
continue
fi
SKIN_NAME=$(echo "$skin_path" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
echo ""
echo "[$INDEX/$SKIN_COUNT] Skin: $SKIN_NAME"
LOGFILE="/tmp/danser_log_$INDEX.txt"
FFMPEG_LOG="/tmp/ffmpeg_log_$INDEX.txt"
echo " → Generating video..."
if ! xvfb-run -a "$DANSER_DIR/danser-cli" \
-replay "$REPLAY_PATH" \
-record -skip -start=215 -end=230 -noupdatecheck \
-out="$SKIN_NAME" -skin="$SKIN_NAME" >"$LOGFILE" 2>&1; then
echo " ✖ Video generation failed for $SKIN_NAME. Log output:"
cat "$LOGFILE"
continue
fi
echo " → Taking screenshot..."
if ! xvfb-run -a "$DANSER_DIR/danser-cli" \
-replay "$REPLAY_PATH" -skip -noupdatecheck -ss 243 \
-out="$SKIN_NAME" -skin="$SKIN_NAME" >>"$LOGFILE" 2>&1; then
echo " ✖ Screenshot generation failed for $SKIN_NAME. Log output:"
cat "$LOGFILE"
continue
fi
if [ -f "$DANSER_VIDEO_DIR/$SKIN_NAME.mp4" ]; then
echo " → Converting to GIF..."
if ! ffmpeg -y -hwaccel cuda -ss 4 -t 10 -i "$DANSER_VIDEO_DIR/$SKIN_NAME.mp4" \
-filter_complex "[0:v] fps=24,scale=720:-1:flags=lanczos,palettegen [p]; [0:v] fps=24,scale=720:-1:flags=lanczos [x]; [x][p] paletteuse" \
-c:v gif "$DANSER_VIDEO_DIR/$SKIN_NAME.gif" >"$FFMPEG_LOG" 2>&1; then
echo " ✖ FFmpeg conversion failed for $SKIN_NAME. Log output:"
cat "$FFMPEG_LOG"
continue
fi
mv "$DANSER_VIDEO_DIR/$SKIN_NAME.gif" "$REPO_SCREENSHOT_DIR/$SKIN_NAME.gif"
else
echo " ✖ Video file not found for $SKIN_NAME."
fi
if [ -f "$DANSER_SCREENSHOT_DIR/$SKIN_NAME.png" ]; then
mv "$DANSER_SCREENSHOT_DIR/$SKIN_NAME.png" "$REPO_RANKING_PANEL_DIR/$SKIN_NAME.png"
else
echo " ✖ Screenshot file not found for $SKIN_NAME."
fi
echo " ✓ Completed"
INDEX=$((INDEX + 1))
done
echo ""
echo "[Danser Job Finished — $SKIN_COUNT skins processed]"
- name: Rename Generated Assets Based on skin.ini
shell: bash
run: |
echo "[Asset Renaming Job Started]"
if [ -z "${CHANGED_SKINS_FILE:-}" ] || [ ! -s "$CHANGED_SKINS_FILE" ]; then
echo "No skins changed. Skipping asset renaming."
exit 0
fi
mapfile -t skins < "$CHANGED_SKINS_FILE"
if [ "${#skins[@]}" -eq 0 ]; then
echo "No skins changed after reading file. Skipping asset renaming."
exit 0
fi
SKIN_COUNT=${#skins[@]}
INDEX=1
sanitize_filename() {
echo "$1" \
| sed -e 's#[\\/:\*\?"<>|]#-#g' -e 's#%#_#g' \
| tr -s ' ' \
| sed 's/^ *//;s/ *$//'
}
for skin_path in "${skins[@]}"; do
[ -z "$skin_path" ] && continue
SKIN_DIR="$DANSER_SKINS_DIR/$skin_path"
[ ! -d "$SKIN_DIR" ] && { echo "Skipping missing skin directory: $SKIN_DIR"; continue; }
SKIN_NAME=$(basename -- "$skin_path" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
echo "Processing skin $INDEX/$SKIN_COUNT: $SKIN_NAME"
ini_file=$(find "$SKIN_DIR" -maxdepth 1 -iname "skin.ini" | head -n1 || true)
skin_header="$SKIN_NAME"
if [ -f "$ini_file" ]; then
name_line=$(grep -i '^[[:space:]]*name:' "$ini_file" | head -n1 || true)
if [ -n "$name_line" ]; then
new_name=$(echo "$name_line" | cut -d':' -f2- \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
new_name=$(sanitize_filename "$new_name")
[ -n "$new_name" ] && skin_header="$new_name"
fi
fi
if [ -f "$REPO_SCREENSHOT_DIR/$SKIN_NAME.gif" ] && [ "$SKIN_NAME" != "$skin_header" ]; then
mv -f "$REPO_SCREENSHOT_DIR/$SKIN_NAME.gif" \
"$REPO_SCREENSHOT_DIR/$skin_header.gif" || true
echo " ✓ Renamed GIF"
fi
if [ -f "$REPO_RANKING_PANEL_DIR/$SKIN_NAME.png" ] && [ "$SKIN_NAME" != "$skin_header" ]; then
mv -f "$REPO_RANKING_PANEL_DIR/$SKIN_NAME.png" \
"$REPO_RANKING_PANEL_DIR/$skin_header.png" || true
echo " ✓ Renamed PNG"
fi
INDEX=$((INDEX + 1))
done
echo ""
echo "[Asset Renaming Complete — $SKIN_COUNT skins processed]"
- name: Generate Mod Icons
shell: bash
run: |
echo "[Mod Icon Generation Job Started]"
if [ -z "${CHANGED_SKINS_FILE:-}" ] || [ ! -s "$CHANGED_SKINS_FILE" ]; then
echo "No skins changed. Skipping mod icon generation."
exit 0
fi
mapfile -t skin_dirs < "$CHANGED_SKINS_FILE"
if [ "${#skin_dirs[@]}" -eq 0 ]; then
echo "No skins changed after reading file. Skipping mod icon generation."
exit 0
fi
sanitize_filename() {
echo "$1" \
| sed -e 's#[\\/:\*\?"<>|]#-#g' -e 's#%#_#g' \
| tr -s ' ' \
| sed 's/^ *//;s/ *$//'
}
ICONS_JSON_FILE="${{ github.workspace }}/.gitea/workflows/icons.json"
group1_icons=$(jq -r '.group1 | join(" ")' "$ICONS_JSON_FILE")
group2_icons=$(jq -r '.group2 | join(" ")' "$ICONS_JSON_FILE")
group3_icons=$(jq -r '.group3 | join(" ")' "$ICONS_JSON_FILE")
BLANK_IMAGE="blank.png"
magick -size "160x160" xc:none "$BLANK_IMAGE"
SKIN_COUNT=${#skin_dirs[@]}
INDEX=1
for skin_path in "${skin_dirs[@]}"; do
SKIN_DIR="$DANSER_SKINS_DIR/$skin_path"
[ ! -d "$SKIN_DIR" ] && { echo "Skipping missing skin directory: $SKIN_DIR"; continue; }
skin_header=$(basename -- "$skin_path" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
ini_file=$(find "$SKIN_DIR" -maxdepth 1 -iname "skin.ini" | head -n1 || true)
if [ -f "$ini_file" ]; then
name_line=$(grep -i '^[[:space:]]*name:' "$ini_file" | head -n1 || true)
if [ -n "$name_line" ]; then
new_name=$(echo "$name_line" | cut -d ':' -f2- \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
new_name=$(sanitize_filename "$new_name")
[ -n "$new_name" ] && skin_header="$new_name"
fi
fi
echo ""
echo "[$INDEX/$SKIN_COUNT] Skin: $skin_header"
ICON_FOLDER="$SKIN_DIR"
OUTPUT="${REPO_MOD_ICONS_DIR}/${skin_header}-mod-icons.png"
row_images=()
row_index=1
for group_list in "$group1_icons" "$group2_icons" "$group3_icons"; do
montage_files=()
for icon in $group_list; do
if [ -f "${ICON_FOLDER}/selection-mod-${icon}@2x.png" ]; then
montage_files+=("${ICON_FOLDER}/selection-mod-${icon}@2x.png")
elif [ -f "${DEFAULT_SKIN_DIR}/selection-mod-${icon}@2x.png" ]; then
montage_files+=("${DEFAULT_SKIN_DIR}/selection-mod-${icon}@2x.png")
fi
done
while [ "${#montage_files[@]}" -lt 7 ]; do
montage_files+=("$BLANK_IMAGE")
done
magick montage "${montage_files[@]}" -tile "7x1" -geometry "160x160+10+10" -background none "row_${row_index}.png"
row_images+=("row_${row_index}.png")
row_index=$((row_index + 1))
done
magick montage "${row_images[@]}" -tile "1x${#row_images[@]}" -geometry "+10+10" -background none "$OUTPUT"
rm row_*.png
echo " ✓ Mod Icons Generated"
INDEX=$((INDEX + 1))
done
rm "$BLANK_IMAGE"
echo ""
echo "[Mod Icon Generation Finished — $SKIN_COUNT skins processed]"
- name: Create OSK Files
shell: bash
run: |
echo "[OSK Creation Job Started]"
if [ -z "${CHANGED_SKINS_FILE:-}" ] || [ ! -s "$CHANGED_SKINS_FILE" ]; then
echo "No skins changed. Skipping OSK creation."
exit 0
fi
mapfile -t skin_dirs < "$CHANGED_SKINS_FILE"
if [ "${#skin_dirs[@]}" -eq 0 ]; then
echo "No skins changed after reading file. Skipping OSK creation."
exit 0
fi
sanitize_filename() {
echo "$1" \
| sed -e 's#[\\/:\*\?"<>|]#-#g' -e 's#%#_#g' \
| tr -s ' ' \
| sed 's/^ *//;s/ *$//'
}
FIXED_TIMESTAMP="2025-01-01 00:00:00"
SKIN_COUNT=${#skin_dirs[@]}
INDEX=1
for skin_path in "${skin_dirs[@]}"; do
SKIN_DIR="$DANSER_SKINS_DIR/$skin_path"
[ ! -d "$SKIN_DIR" ] && { echo "Skipping missing skin directory: $SKIN_DIR"; continue; }
skin_header=$(basename -- "$skin_path" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
ini_file=$(find "$SKIN_DIR" -maxdepth 1 -iname "skin.ini" | head -n1 || true)
if [ -f "$ini_file" ]; then
name_line=$(grep -i '^[[:space:]]*name:' "$ini_file" | head -n1 || true)
if [ -n "$name_line" ]; then
new_name=$(echo "$name_line" | cut -d ':' -f2- \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
new_name=$(sanitize_filename "$new_name")
[ -n "$new_name" ] && skin_header="$new_name"
fi
fi
echo ""
echo "[$INDEX/$SKIN_COUNT] Processing skin: $skin_header"
(cd "$SKIN_DIR" && find . -type f -exec touch -d "$FIXED_TIMESTAMP" {} +)
(cd "$SKIN_DIR" && find . -type f | sort | zip -rq -D -X -9 --compression-method deflate "$OSK_PATH/${skin_header}.osk" -@)
echo " ✓ OSK file created successfully."
INDEX=$((INDEX + 1))
done
echo ""
echo "[OSK Creation Job Finished — $SKIN_COUNT skins processed]"
- name: Generate README
shell: bash
run: |
echo "Starting README generation..."
sanitize_filename() {
echo "$1" | \
sed -e 's#[\\/:\*\?"<>|]#-#g' -e 's#%#_#g' | \
tr -s ' ' | \
sed 's/^ *//;s/ *$//'
}
SKINS_JSON_FILE="${{ github.workspace }}/.gitea/workflows/skins.json"
DESC_FILE=$(mktemp)
echo "Step 1: Extracting descriptions from skins.json..."
jq -r '.descriptions | to_entries[] | "\(.key)=\(.value)"' "$SKINS_JSON_FILE" > "$DESC_FILE"
echo "Step 2: Starting to build README..."
echo "---" > "$README_PATH"
echo "gitea: none" >> "$README_PATH"
echo "include_toc: true" >> "$README_PATH"
echo "---" >> "$README_PATH"
echo "" >> "$README_PATH"
echo "# Skins" >> "$README_PATH"
echo "" >> "$README_PATH"
echo "<!--" >> "$README_PATH"
echo "osuid: $OSU_ID" >> "$README_PATH"
echo "-->" >> "$README_PATH"
echo "" >> "$README_PATH"
echo "**Go back to [osc/skins](https://git.sulejmani.xyz/osc/skins)**" >> "$README_PATH"
echo "" >> "$README_PATH"
get_desc() {
key=$1
grep "^${key}=" "$DESC_FILE" 2>/dev/null | cut -d '=' -f2-
}
ORDER_FILE=$(mktemp)
JSON_SKINS_TMP=$(mktemp)
SEEN_HEADERS_FILE=$(mktemp)
echo "Step 3: Extracting order from skins.json..."
jq -r '.order[]' "$SKINS_JSON_FILE" > "$ORDER_FILE"
cp "$ORDER_FILE" "$JSON_SKINS_TMP"
echo "Step 4: Processing ordered skins..."
while IFS= read -r skin; do
echo " Processing skin (order): $skin"
dir="$DANSER_SKINS_DIR/$skin"
if [ ! -d "$dir" ]; then
echo " Skipping missing directory: $dir"
continue
fi
ini_file=$(find "$dir" -maxdepth 1 -iname "skin.ini" | head -n 1 || true)
skin_header="$skin"
if [ -f "$ini_file" ]; then
name_line=$(grep -i '^[[:space:]]*name:' "$ini_file" | head -n 1 || true)
if [ -n "${name_line:-}" ]; then
new_name=$(echo "$name_line" | cut -d ':' -f2- | sed 's/^[[:space:]]*//')
new_name=$(sanitize_filename "$new_name")
[ -n "$new_name" ] && skin_header="$new_name"
fi
fi
skin_header=$(printf '%s' "$skin_header" | tr -d '\r\n' | sed -e 's/[[:space:]]*$//')
if grep -Fxq "$skin_header" "$SEEN_HEADERS_FILE"; then
echo " Already seen skin header: $skin_header"
continue
fi
echo "$skin_header" >> "$SEEN_HEADERS_FILE"
escaped_img=$(printf "%s" "$skin_header.gif" | sed 's/ /%20/g')
escaped_osk=$(printf "%s" "$skin_header.osk" | sed 's/ /%20/g')
echo " Writing skin: $skin_header"
echo "## [$skin_header]($REGISTRY_URL/$USER_REPOSITORY/media/tag/$new_tag/export/$escaped_osk)" >> "$README_PATH"
echo "" >> "$README_PATH"
skin_desc=$(get_desc "$skin")
if [ -n "$skin_desc" ]; then
echo "$skin_desc" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
if [ -f "$ini_file" ]; then
author_line=$(grep -i '^[[:space:]]*Author:' "$ini_file" | head -n 1 || true)
if [ -n "${author_line:-}" ]; then
author=$(echo "$author_line" | cut -d ':' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$author" ]; then
echo "**Author:** $author" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
fi
fi
echo "![$skin_header Gameplay](media/gameplay/$escaped_img)" >> "$README_PATH"
echo "" >> "$README_PATH"
if [ -f "media/panel/${skin_header}.png" ]; then
escaped_panel=$(printf "%s" "${skin_header}.png" | sed 's/ /%20/g')
echo "![$skin_header Ranking Panel](media/panel/$escaped_panel)" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
mod_icon_file="${skin_header}-mod-icons.png"
if [ -f "media/icons/$mod_icon_file" ]; then
escaped_mod=$(printf "%s" "$mod_icon_file" | sed 's/ /%20/g')
echo "![$skin_header Mods](media/icons/$escaped_mod)" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
done < "$ORDER_FILE"
echo "Step 5: Processing extra skins..."
find "$DANSER_SKINS_DIR" -mindepth 1 -maxdepth 1 -type d | while IFS= read -r dir; do
skin=$(basename "$dir")
echo " Processing extra skin: $skin"
ini_file=$(find "$dir" -maxdepth 1 -iname "skin.ini" | head -n 1 || true)
skin_header="$skin"
if [ -f "$ini_file" ]; then
name_line=$(grep -i '^[[:space:]]*name:' "$ini_file" | head -n 1 || true)
if [ -n "${name_line:-}" ]; then
new_name=$(echo "$name_line" | cut -d ':' -f2- | sed 's/^[[:space:]]*//')
new_name=$(sanitize_filename "$new_name")
[ -n "$new_name" ] && skin_header="$new_name"
fi
fi
skin_header=$(printf '%s' "$skin_header" | tr -d '\r\n' | sed -e 's/[[:space:]]*$//')
if grep -Fxq "$skin_header" "$SEEN_HEADERS_FILE"; then
echo " Already seen (extra): $skin_header"
continue
fi
if grep -Fxq "$skin" "$JSON_SKINS_TMP"; then
echo " Already ordered (extra): $skin"
continue
fi
echo "$skin_header" >> "$SEEN_HEADERS_FILE"
escaped_img=$(printf "%s" "$skin_header.gif" | sed 's/ /%20/g')
escaped_osk=$(printf "%s" "$skin_header.osk" | sed 's/ /%20/g')
echo " Writing extra skin: $skin_header"
echo "## [$skin_header]($REGISTRY_URL/$USER_REPOSITORY/media/tag/$new_tag/export/$escaped_osk)" >> "$README_PATH"
echo "" >> "$README_PATH"
if [ -f "$ini_file" ]; then
author_line=$(grep -i '^[[:space:]]*Author:' "$ini_file" | head -n 1 || true)
if [ -n "${author_line:-}" ]; then
author=$(echo "$author_line" | cut -d ':' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$author" ]; then
echo "**Author:** $author" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
fi
fi
echo "![$skin_header Gameplay](media/gameplay/$escaped_img)" >> "$README_PATH"
echo "" >> "$README_PATH"
if [ -f "media/panel/${skin_header}.png" ]; then
escaped_panel=$(printf "%s" "${skin_header}.png" | sed 's/ /%20/g')
echo "![$skin_header Ranking Panel](media/panel/$escaped_panel)" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
mod_icon_file="${skin_header}-mod-icons.png"
if [ -f "media/icons/$mod_icon_file" ]; then
escaped_mod=$(printf "%s" "$mod_icon_file" | sed 's/ /%20/g')
echo "![$skin_header Mods](media/icons/$escaped_mod)" >> "$README_PATH"
echo "" >> "$README_PATH"
fi
done
echo "Step 7: Writing Build History section..."
echo "# Build History" >> "$README_PATH"
echo "" >> "$README_PATH"
echo "| Version | Date |" >> "$README_PATH"
echo "| ------- | ---- |" >> "$README_PATH"
echo " Getting latest commit date..."
current_commit_date=$(TZ="Europe/Zurich" date -d "$(git log -1 --format=%cI)" "+%d.%m.%Y %H:%M:%S")
echo " Latest commit date: $current_commit_date"
echo "| [\`$new_tag (Current)\`](https://git.sulejmani.xyz/arlind/skins/src/tag/$new_tag/README.md) | $current_commit_date |" >> "$README_PATH"
echo " Checking for old tags..."
old_tags=$(git tag --sort=-v:refname | grep -v "^$new_tag$" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true)
if [ -n "$old_tags" ]; then
echo " Found old tags:"
echo "$old_tags" | while read -r tag; do
echo " Processing tag: $tag"
tag_date=$(git log -1 --format=%ci "$tag")
formatted_date=$(TZ="Europe/Zurich" date -d "$tag_date" "+%d.%m.%Y %H:%M:%S")
echo "| [\`$tag\`](https://git.sulejmani.xyz/arlind/skins/src/tag/$tag/README.md) | $formatted_date |" >> "$README_PATH"
done
else
echo " No old tags found. Skipping old tags section."
fi
echo "README generation completed successfully."
- name: Cleanup Extra Files
shell: bash
run: |
set -euo pipefail
echo "[Cleanup Extra Files Started]"
rm -rf src/docs || true
rm -f how-to-use.md || true
sanitize_filename() {
echo "$1" \
| sed -e 's#[\\/:\*\?"<>|]#-#g' -e 's#%#_#g' \
| tr -s ' ' \
| sed 's/^ *//;s/ *$//'
}
expected_basenames=()
for dir in "$DANSER_SKINS_DIR"/*; do
[ -d "$dir" ] || continue
raw=$(basename "$dir" | tr -d '\r\n')
header=$(sanitize_filename "$raw")
expected_basenames+=("$header")
if ini=$(find "$dir" -maxdepth 1 -iname skin.ini | head -n1); then
if name_line=$(grep -i '^[[:space:]]*name:' "$ini" | head -n1); then
val="${name_line#*:}"
val=$(echo "$val" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
header=$(sanitize_filename "$val")
expected_basenames+=("$header")
fi
fi
done
readarray -t expected_basenames < <(
printf "%s\n" "${expected_basenames[@]}" | sort -u
)
for b in "${expected_basenames[@]}"; do
expected_basenames+=("${b}-mod-icons")
done
readarray -t expected_basenames < <(
printf "%s\n" "${expected_basenames[@]}" | sort -u
)
prune_dir() {
for f in "$1"/*; do
[ -e "$f" ] || continue
base="${f##*/}"
base="${base%.*}"
keep=false
for kb in "${expected_basenames[@]}"; do
if [[ "$base" == "$kb" ]]; then
keep=true
break
fi
done
$keep || rm -rf "$f"
done
}
prune_dir "$REPO_SCREENSHOT_DIR"
prune_dir "$REPO_RANKING_PANEL_DIR"
prune_dir "$REPO_MOD_ICONS_DIR"
prune_dir "$OSK_PATH"
echo "[Cleanup Extra Files Complete]"
- name: Configure Git
shell: bash
run: |
git config user.email "arlind@sulej.ch"
git config user.name "ci-bot"
git config lfs.https://${{ vars.CONTAINER_REGISTRY }}/arlind/skins.git/info/lfs.locksverify true
- name: Add and Commit changes
shell: bash
run: |
git config advice.addIgnoredFile false
for p in media/gameplay media/panel media/icons export README.md how-to-use.md src; do
if [ -e "$p" ]; then
git add -A "$p"
fi
done
git commit -m "[ci skip] push back from pipeline" -q || echo "No changes to commit"
- name: Push changes and create tag
shell: bash
run: |
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
git push origin HEAD:main || echo "No changes to push"
git tag "$new_tag"
git push origin "$new_tag"
else
git push origin HEAD:"${GITHUB_REF_NAME}" || echo "No changes to push"
fi

View File

@@ -0,0 +1,25 @@
{
"group1": [
"easy",
"nofail",
"halftime"
],
"group2": [
"hardrock",
"suddendeath",
"perfect",
"doubletime",
"nightcore",
"hidden",
"flashlight"
],
"group3": [
"relax",
"relax2",
"autoplay",
"target",
"spunout",
"cinema",
"scorev2"
]
}

View File

@@ -0,0 +1,10 @@
{
"order": [
"example1",
"example2"
],
"descriptions": {
"example1": "Description of example1",
"example2": "Description of example2"
}
}

0
.gitignore vendored Normal file
View File

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Arlind-dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

15
hardlink-songs-folder.bat Normal file
View File

@@ -0,0 +1,15 @@
@echo off
net session >nul 2>&1
if %errorLevel% neq 0 (
echo Requesting admin privileges...
powershell -Command "Start-Process cmd -ArgumentList '/c \"%~f0\"' -Verb RunAs"
exit /b
)
echo Running robocopy sync as Administrator...
echo.
robocopy "E:\osu!\skins" "D:\git\skins\Skins" /MIR /COPYALL /SEC /B /XJ /DCOPY:T /J
echo Robocopy sync completed.
pause

6
rsync.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
rsync -av --delete /mnt/e/osu\!/Skins/ /home/nixos/git/skins/Skins/
find ./Skins/ -type f ! -perm 644 -exec chmod 644 {} +
find ./Skins/ -type d ! -perm 755 -exec chmod 755 {} +

View File

@@ -0,0 +1,7 @@
All osu! graphic and other game assets are copyright ppy Pty Ltd.
You are granted permission to use the elements contained within this archive as a template for creating your own skins. You can modify these in any way you see fit. Please do not include the resources if you haven't modified them; the defaults will automatically be used in this case.
You are NOT permitted to use these graphics outside of skins and/or beatmaps. This includes using them on other websites, games, products etc.
If you would like to use the resources outside of the scope provided above, please contact me at pe@ppy.sh

View File

@@ -0,0 +1,27 @@
About this skin :
=======================
Template by Corne2Plum3 (https://osu.ppy.sh/users/15646039).
This is the just the default skin from the stable version (no target practice sprites sorry...), but 2021 version (from the 20210821 version to be exact). I tried to make the default skin but updated (the last I found was from 2014...), and complete (I added almost missing files) where skinners use it as a template to create their skin.
Most of the sprites and gameplay sounds are from ppy/osu-ressources (https://github.com/ppy/osu-resources). Sometimes I used an AI to generates the @2x versions of some files. For the sounds, I tried to pick them directly from the game, so sorry if it's not accurate...
Please don't kill me for making this, peppy!
A few notes :
=============
BACK BUTTON:
It's impossible to recreate the back button. In this skin, there is just 1 frame of the back button. Unlike the default, he's isn't animated, and doesn't change according to the game language.
COMBO SPRITES:
Same than score sprites.
SLIDERBALL:
It's IMPOSSIBLE to recreate exactly the default sliderball with skinning, because sliderb files disable sliderb-spec (overlay) and sliderb-nd (background). So you will see only sliderb files in game with this skin.
The only way to have the same sliderball than the default skin is deleting ALL slider ball files (sliderb, sliderb-spec and sliderb-nd).
SPINNER APPROACH CIRCLE:
With the default skin which uses new spinner, spinner-approachcircle is invisible. In the folder 'new spinner/visible approach circle' you have the default spinner-approachcircle if this file is needed.

BIN
src/default-skin/applause.wav LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/button-left.png LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/button-right.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/check-off.wav LFS Normal file

Binary file not shown.

BIN
src/default-skin/check-on.wav LFS Normal file

Binary file not shown.

BIN
src/default-skin/click-close.wav LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/click-short.wav LFS Normal file

Binary file not shown.

BIN
src/default-skin/combobreak.wav LFS Normal file

Binary file not shown.

BIN
src/default-skin/comboburst.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/count.wav LFS Normal file

Binary file not shown.

BIN
src/default-skin/count1.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/count1@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/count2.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/count2@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/count3.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/count3@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/cursor-smoke.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/cursor.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/cursor@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/cursormiddle.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/cursortrail.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/default-0.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-0@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-1.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-1@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-2.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-2@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-3.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-3@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-4.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-4@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-5.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-5@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-6.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-6@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-7.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-7@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-8.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-8@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-9.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/default-9@2x.png LFS Normal file

Binary file not shown.

BIN
src/default-skin/drum-hitclap.wav LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/failsound.mp3 LFS Normal file

Binary file not shown.

BIN
src/default-skin/followpoint.png LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/fruit-apple.png LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/fruit-drop.png LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/fruit-grapes.png LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/default-skin/fruit-orange.png LFS Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More