Compare commits
22 Commits
8fc3803467
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5deccc54a7 | ||
|
|
2455d9724b | ||
|
|
ac7c1b9ce1 | ||
|
|
d68d76c854 | ||
|
|
f1927cc67e | ||
|
|
68ab4627d8 | ||
|
|
0d8d869580 | ||
|
|
a31b85b5df | ||
|
|
21b08d4866 | ||
|
|
f075caaa17 | ||
|
|
9422168ffc | ||
|
|
35e557e298 | ||
|
|
e810419063 | ||
|
|
f7e7dee28f | ||
|
|
4b0b8f4f2e | ||
|
|
03249c88a4 | ||
|
|
2d4f97e04b | ||
|
|
317fce796c | ||
|
|
9832abd131 | ||
|
|
c94ebc6b4b | ||
|
|
8b23c1c7bd | ||
|
|
5e325a4691 |
@@ -1,23 +1,17 @@
|
||||
[target.'cfg(target_os = "emscripten")']
|
||||
rustflags = [
|
||||
# Stack size is required for this project, it will crash otherwise.
|
||||
"-C",
|
||||
"link-args=-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=8192 -sALLOW_MEMORY_GROWTH=1",
|
||||
"-C",
|
||||
"link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sUSE_OGG=1 -sUSE_SDL_GFX=2 -sUSE_SDL_TTF=2 -sSDL2_IMAGE_FORMATS=['png']",
|
||||
"-C",
|
||||
"link-args=--preload-file pacman/assets/game/",
|
||||
"-C", "link-args=-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=8192 -sALLOW_MEMORY_GROWTH=1",
|
||||
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sUSE_OGG=1 -sUSE_SDL_GFX=2 -sUSE_SDL_TTF=2 -sSDL2_IMAGE_FORMATS=['png']",
|
||||
"-C", "link-args=--preload-file assets/game/",
|
||||
]
|
||||
runner = "node"
|
||||
|
||||
# despite being semantically identical to `target_os = "linux"`, the `cfg(linux)` syntax is not supported here. Who knows why...
|
||||
# https://github.com/Xevion/Pac-Man/actions/runs/17596477856
|
||||
[target.'cfg(target_os = "linux")']
|
||||
rustflags = [
|
||||
# Manually link zlib.
|
||||
# The `sdl2` crate's build script uses `libpng`, which requires `zlib`.
|
||||
# By adding `-lz` here, we ensure it's passed to the linker after `libpng`,
|
||||
# which is required for the linker to correctly resolve symbols.
|
||||
"-C",
|
||||
"link-arg=-lz",
|
||||
"-C", "link-arg=-lz",
|
||||
]
|
||||
|
||||
@@ -1,28 +1,5 @@
|
||||
[profile.default]
|
||||
fail-fast = false
|
||||
slow-timeout = { period = "5s", terminate-after = 3 } # max 15 seconds
|
||||
retries = 1
|
||||
|
||||
# CI machines are pretty slow, so we need to increase the timeout
|
||||
[profile.ci]
|
||||
slow-timeout = { period = "30s", terminate-after = 4 } # max 2 minutes for slow tests
|
||||
|
||||
# Coverage works even slower, so we need to increase the timeout
|
||||
[profile.coverage]
|
||||
slow-timeout = { period = "45s", terminate-after = 5 } # max 3.75 minutes for slow tests
|
||||
status-level = "none"
|
||||
|
||||
# Integration tests in SDL2 run serially (may not be required)
|
||||
[[profile.default.overrides]]
|
||||
filter = 'test(pacman::game::)'
|
||||
test-group = 'serial'
|
||||
|
||||
# Integration tests run max 4 at a time
|
||||
[[profile.default.overrides]]
|
||||
filter = 'test(pacman-server::tests::oauth)'
|
||||
test-group = 'integration'
|
||||
|
||||
[test-groups]
|
||||
# Ensure serial tests don't run in parallel
|
||||
serial = { max-threads = 1 }
|
||||
integration = { max-threads = 4 }
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Build artifacts
|
||||
/target
|
||||
/emsdk
|
||||
*.exe
|
||||
|
||||
/pacman/assets
|
||||
/assets
|
||||
|
||||
# Development files
|
||||
/.git
|
||||
/*.md
|
||||
/Justfile
|
||||
/bacon.toml
|
||||
/rust-toolchain.toml
|
||||
/rustfmt.toml
|
||||
1
.gitattributes
vendored
@@ -1,2 +1 @@
|
||||
* text=auto eol=lf
|
||||
scripts/* linguist-detectable=false
|
||||
|
||||
80
.github/dependabot.yml
vendored
@@ -1,86 +1,20 @@
|
||||
# Dependabot Configuration
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
#
|
||||
# Strategy:
|
||||
# - Weekly checks for faster vulnerability detection
|
||||
# - Separate patch/minor/major updates to prevent blocking
|
||||
# - Auto-merge patches via GitHub branch protection rules
|
||||
# - Limit concurrent PRs to avoid spam
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Cargo workspace (all Rust crates)
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 5
|
||||
ignore:
|
||||
# Bevy ECS 0.17+ requires API migration
|
||||
- dependency-name: "bevy_ecs"
|
||||
versions: ["0.17.x", "0.18.x", "0.19.x"]
|
||||
# jsonwebtoken 10+ requires crypto backend feature flag
|
||||
- dependency-name: "jsonwebtoken"
|
||||
versions: ["10.x", "11.x"]
|
||||
interval: "monthly"
|
||||
groups:
|
||||
rust-patches:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["patch"]
|
||||
rust-minor:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["minor"]
|
||||
rust-major:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["major"]
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "rust"
|
||||
|
||||
# Frontend (web/)
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/web"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
frontend-patches:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["patch"]
|
||||
frontend-minor:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["minor"]
|
||||
frontend-major-framework:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["major"]
|
||||
dependencies:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "vike"
|
||||
- "vite"
|
||||
frontend-major-other:
|
||||
applies-to: "version-updates"
|
||||
update-types: ["major"]
|
||||
exclude-patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "vike"
|
||||
- "vite"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "frontend"
|
||||
|
||||
# GitHub Actions
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 5
|
||||
interval: "monthly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns: ["*"]
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
126
.github/workflows/build.yaml
vendored
@@ -1,16 +1,8 @@
|
||||
name: Builds
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
on: ["push", "pull_request"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,11 +15,11 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact_name: pacman
|
||||
toolchain: 1.86.0
|
||||
- os: macos-15-intel
|
||||
- os: macos-13
|
||||
target: x86_64-apple-darwin
|
||||
artifact_name: pacman
|
||||
toolchain: 1.86.0
|
||||
- os: macos-15
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
artifact_name: pacman
|
||||
toolchain: 1.86.0
|
||||
@@ -38,7 +30,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Rust Toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -49,23 +41,13 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Get vcpkg baseline
|
||||
id: vcpkg_version
|
||||
shell: bash
|
||||
run: |
|
||||
VCPKG_REV=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "pacman") | .metadata.vcpkg.rev // "unknown"')
|
||||
echo "version=$VCPKG_REV" >> $GITHUB_OUTPUT
|
||||
working-directory: pacman
|
||||
|
||||
- name: Cache vcpkg
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
target/vcpkg
|
||||
~/.cargo/bin/cargo-vcpkg
|
||||
key: vcpkg-${{ steps.vcpkg_version.outputs.version }}-${{ runner.os }}-${{ matrix.target }}
|
||||
path: target/vcpkg
|
||||
key: A-vcpkg-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
|
||||
restore-keys: |
|
||||
vcpkg-${{ steps.vcpkg_version.outputs.version }}-${{ runner.os }}-
|
||||
A-vcpkg-${{ runner.os }}-${{ matrix.target }}-
|
||||
|
||||
- name: Vcpkg Linux Dependencies
|
||||
if: runner.os == 'Linux'
|
||||
@@ -73,17 +55,13 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libltdl-dev
|
||||
|
||||
- name: Setup vcpkg
|
||||
shell: bash
|
||||
- name: Vcpkg
|
||||
run: |
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
echo "VCPKG_ROOT=${{ github.workspace }}/target/vcpkg" >> $GITHUB_ENV
|
||||
working-directory: pacman
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
working-directory: pacman
|
||||
|
||||
- name: Acquire Package Version
|
||||
id: get_version
|
||||
@@ -91,12 +69,94 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail # exit on error
|
||||
echo "version=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)" >> $GITHUB_OUTPUT
|
||||
working-directory: pacman
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "pacman-${{ steps.get_version.outputs.version }}-${{ matrix.target }}"
|
||||
path: ./target/release/${{ matrix.artifact_name }}
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
wasm:
|
||||
name: Build (wasm32-unknown-emscripten)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
# concurrency group is used to prevent multiple page deployments from being attempted at the same time
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-wasm
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Emscripten SDK
|
||||
uses: pyodide/setup-emsdk@v15
|
||||
with:
|
||||
version: 3.1.43
|
||||
actions-cache-folder: "emsdk-cache-b"
|
||||
|
||||
- name: Setup Rust (WASM32 Emscripten)
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
target: wasm32-unknown-emscripten
|
||||
toolchain: 1.86.0
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Build with Emscripten
|
||||
shell: bash
|
||||
run: |
|
||||
# Retry mechanism for Emscripten build - only retry on specific hash errors
|
||||
MAX_RETRIES=3
|
||||
RETRY_DELAY=30
|
||||
|
||||
for attempt in $(seq 1 $MAX_RETRIES); do
|
||||
echo "Build attempt $attempt of $MAX_RETRIES"
|
||||
|
||||
# Capture output and check for specific error while preserving real-time output
|
||||
if bun run -i web.build.ts 2>&1 | tee /tmp/build_output.log; then
|
||||
echo "Build successful on attempt $attempt"
|
||||
break
|
||||
else
|
||||
echo "Build failed on attempt $attempt"
|
||||
|
||||
# Check if the failure was due to the specific hash error
|
||||
if grep -q "emcc: error: Unexpected hash:" /tmp/build_output.log; then
|
||||
echo "::warning::Detected 'emcc: error: Unexpected hash:' error - will retry (attempt $attempt of $MAX_RETRIES)"
|
||||
|
||||
if [ $attempt -eq $MAX_RETRIES ]; then
|
||||
echo "::error::All retry attempts failed. Exiting with error."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting $RETRY_DELAY seconds before retry..."
|
||||
sleep $RETRY_DELAY
|
||||
|
||||
# Exponential backoff: double the delay for next attempt
|
||||
RETRY_DELAY=$((RETRY_DELAY * 2))
|
||||
else
|
||||
echo "Build failed but not due to hash error - not retrying"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
with:
|
||||
path: "./dist/"
|
||||
retention-days: 7
|
||||
|
||||
- name: Deploy
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
64
.github/workflows/checks.yaml
vendored
@@ -1,64 +0,0 @@
|
||||
name: Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_TOOLCHAIN: 1.86.0
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Cache vcpkg
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target/vcpkg
|
||||
key: A-vcpkg-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
|
||||
restore-keys: |
|
||||
A-vcpkg-${{ runner.os }}-
|
||||
|
||||
- name: Vcpkg Linux Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libltdl-dev
|
||||
|
||||
- name: Vcpkg
|
||||
run: |
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
working-directory: pacman
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
working-directory: pacman
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
working-directory: pacman
|
||||
|
||||
- uses: taiki-e/install-action@cargo-audit
|
||||
|
||||
- name: Run security audit
|
||||
run: cargo audit
|
||||
working-directory: pacman
|
||||
56
.github/workflows/coverage.yaml
vendored
@@ -1,23 +1,19 @@
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
on: ["push", "pull_request"]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_TOOLCHAIN: nightly
|
||||
RUST_TOOLCHAIN: 1.86.0
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -45,7 +41,6 @@ jobs:
|
||||
run: |
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
working-directory: pacman
|
||||
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: taiki-e/install-action@nextest
|
||||
@@ -54,11 +49,40 @@ jobs:
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
just coverage
|
||||
working-directory: pacman
|
||||
|
||||
- name: Coveralls upload
|
||||
uses: coverallsapp/github-action@v2
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
path-to-lcov: lcov.info
|
||||
debug: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: lcov.info
|
||||
|
||||
- name: Download Coveralls CLI
|
||||
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
|
||||
run: |
|
||||
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
|
||||
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
|
||||
run: |
|
||||
if [ ! -f "lcov.info" ]; then
|
||||
echo "Error: lcov.info file not found. Coverage generation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in {1..10}; do
|
||||
echo "Attempt $i: Uploading coverage to Coveralls..."
|
||||
if coveralls -n report lcov.info; then
|
||||
echo "Successfully uploaded coverage report."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $i -lt 10 ]; then
|
||||
delay=$((2**i))
|
||||
echo "Attempt $i failed. Retrying in $delay seconds..."
|
||||
sleep $delay
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Failed to upload coverage report after 10 attempts."
|
||||
exit 1
|
||||
|
||||
139
.github/workflows/deploy.yaml
vendored
@@ -1,139 +0,0 @@
|
||||
name: Deploy to Railway
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build and Deploy
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
digest: ${{ steps.docker_build.outputs.digest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Emscripten SDK
|
||||
uses: pyodide/setup-emsdk@v15
|
||||
with:
|
||||
version: 3.1.43
|
||||
actions-cache-folder: "emsdk-cache-b"
|
||||
|
||||
- name: Setup Rust (WASM32 Emscripten)
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
target: wasm32-unknown-emscripten
|
||||
toolchain: 1.86.0
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
# ========== WASM Build ==========
|
||||
- name: Build WASM with Emscripten
|
||||
shell: bash
|
||||
run: |
|
||||
# Retry mechanism for Emscripten build - only retry on specific hash errors
|
||||
MAX_RETRIES=3
|
||||
RETRY_DELAY=30
|
||||
|
||||
for attempt in $(seq 1 $MAX_RETRIES); do
|
||||
echo "Build attempt $attempt of $MAX_RETRIES"
|
||||
|
||||
# Capture output and check for specific error while preserving real-time output
|
||||
if bun run -i pacman/web.build.ts 2>&1 | tee /tmp/build_output.log; then
|
||||
echo "Build successful on attempt $attempt"
|
||||
break
|
||||
else
|
||||
echo "Build failed on attempt $attempt"
|
||||
|
||||
# Check if the failure was due to the specific hash error
|
||||
if grep -q "emcc: error: Unexpected hash:" /tmp/build_output.log; then
|
||||
echo "::warning::Detected 'emcc: error: Unexpected hash:' error - will retry (attempt $attempt of $MAX_RETRIES)"
|
||||
|
||||
if [ $attempt -eq $MAX_RETRIES ]; then
|
||||
echo "::error::All retry attempts failed. Exiting with error."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting $RETRY_DELAY seconds before retry..."
|
||||
sleep $RETRY_DELAY
|
||||
|
||||
# Exponential backoff: double the delay for next attempt
|
||||
RETRY_DELAY=$((RETRY_DELAY * 2))
|
||||
else
|
||||
echo "Build failed but not due to hash error - not retrying"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# ========== Docker Build and Push ==========
|
||||
# Note: Frontend is now built inside Docker using multi-stage build
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./pacman-server/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
GIT_COMMIT_SHA=${{ github.sha }}
|
||||
|
||||
# Wait for ghcr.io propagation (paranoid safety)
|
||||
- name: Wait for registry propagation
|
||||
run: sleep 5
|
||||
|
||||
# Deploy to Railway - separate job to use container properly
|
||||
deploy:
|
||||
name: Deploy to Railway
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-deploy
|
||||
container: ghcr.io/railwayapp/cli:latest
|
||||
env:
|
||||
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||
steps:
|
||||
- name: Generate proxy Dockerfile
|
||||
run: echo "FROM ghcr.io/xevion/pac-man@${{ needs.build-and-deploy.outputs.digest }}" > Dockerfile
|
||||
|
||||
- name: Deploy to Railway
|
||||
run: railway up --service pac-man
|
||||
29
.github/workflows/tests.yaml
vendored
@@ -1,13 +1,6 @@
|
||||
name: Tests
|
||||
name: Tests & Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
on: ["push", "pull_request"]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -19,12 +12,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -46,10 +40,19 @@ jobs:
|
||||
run: |
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
working-directory: pacman
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run nextest
|
||||
run: cargo nextest run --workspace --profile ci
|
||||
working-directory: pacman
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: taiki-e/install-action@cargo-audit
|
||||
|
||||
- name: Run security audit
|
||||
run: cargo audit
|
||||
|
||||
17
.gitignore
vendored
@@ -1,36 +1,21 @@
|
||||
# IDE, Other files
|
||||
.vscode
|
||||
.idea
|
||||
.claude/
|
||||
rust-sdl2-emscripten/
|
||||
|
||||
# Build files
|
||||
target/
|
||||
dist/
|
||||
emsdk/
|
||||
node_modules/
|
||||
|
||||
# Emscripten build outputs (generated by cargo build)
|
||||
web/public/pacman.data
|
||||
web/public/pacman.js
|
||||
web/public/pacman.wasm
|
||||
web/public/pacman.wasm.map
|
||||
|
||||
# Site build f iles
|
||||
tailwindcss-*
|
||||
pacman/assets/site/build.css
|
||||
assets/site/build.css
|
||||
|
||||
# Coverage reports
|
||||
lcov.info
|
||||
codecov.json
|
||||
coverage.html
|
||||
|
||||
# Profiling output
|
||||
flamegraph.svg
|
||||
/profile.*
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Sensitive
|
||||
*.env
|
||||
|
||||
@@ -12,13 +12,6 @@ repos:
|
||||
- id: forbid-submodules
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: []
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
@@ -27,17 +20,15 @@ repos:
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false
|
||||
|
||||
- id: cargo-check
|
||||
name: cargo check
|
||||
entry: cargo check --workspace --all-targets
|
||||
entry: cargo check --all-targets
|
||||
language: system
|
||||
types_or: [rust, cargo, cargo-lock]
|
||||
pass_filenames: false
|
||||
|
||||
- id: cargo-check-wasm
|
||||
name: cargo check for wasm32-unknown-emscripten
|
||||
entry: cargo check -p pacman --all-targets --target=wasm32-unknown-emscripten
|
||||
entry: cargo check --all-targets --target=wasm32-unknown-emscripten
|
||||
language: system
|
||||
types_or: [rust, cargo, cargo-lock]
|
||||
pass_filenames: false
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import { $ } from "bun";
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { createInterface } from "readline";
|
||||
|
||||
// Constants for container and volume names
|
||||
const CONTAINER_NAME = "pacman-server-postgres";
|
||||
const VOLUME_NAME = "pacman-postgres-data";
|
||||
|
||||
// Helper function to get user input
|
||||
async function getUserChoice(
|
||||
prompt: string,
|
||||
choices: string[],
|
||||
defaultIndex: number = 1
|
||||
): Promise<string> {
|
||||
// Check if we're in an interactive TTY
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log(
|
||||
"Non-interactive environment detected; selecting default option " +
|
||||
defaultIndex
|
||||
);
|
||||
return String(defaultIndex);
|
||||
}
|
||||
|
||||
console.log(prompt);
|
||||
choices.forEach((choice, index) => {
|
||||
console.log(`${index + 1}. ${choice}`);
|
||||
});
|
||||
|
||||
// Use readline for interactive input
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const askForChoice = () => {
|
||||
rl.question("Enter your choice (1-3): ", (answer) => {
|
||||
const choice = answer.trim();
|
||||
if (["1", "2", "3"].includes(choice)) {
|
||||
rl.close();
|
||||
resolve(choice);
|
||||
} else {
|
||||
console.log("Invalid choice. Please enter 1, 2, or 3.");
|
||||
askForChoice();
|
||||
}
|
||||
});
|
||||
};
|
||||
askForChoice();
|
||||
});
|
||||
}
|
||||
|
||||
// Get repository root path from script location
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const repoRoot = join(__dirname, "..");
|
||||
const envPath = join(repoRoot, "pacman-server", ".env");
|
||||
|
||||
console.log("Checking for .env file...");
|
||||
|
||||
// Check if .env file exists and read it
|
||||
let envContent = "";
|
||||
let envLines: string[] = [];
|
||||
let databaseUrlLine = -1;
|
||||
let databaseUrlValue = "";
|
||||
|
||||
if (existsSync(envPath)) {
|
||||
console.log("Found .env file, reading...");
|
||||
envContent = readFileSync(envPath, "utf-8");
|
||||
envLines = envContent.split("\n");
|
||||
|
||||
// Parse .env file for DATABASE_URL
|
||||
for (let i = 0; i < envLines.length; i++) {
|
||||
const line = envLines[i].trim();
|
||||
if (line.match(/^[A-Z_][A-Z0-9_]*=.*$/)) {
|
||||
if (line.startsWith("DATABASE_URL=")) {
|
||||
databaseUrlLine = i;
|
||||
databaseUrlValue = line.substring(13); // Remove "DATABASE_URL="
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("No .env file found, will create one");
|
||||
}
|
||||
|
||||
// Determine user's choice
|
||||
let userChoice = "2"; // Default to print
|
||||
|
||||
if (databaseUrlLine !== -1) {
|
||||
console.log(`Found existing DATABASE_URL: ${databaseUrlValue}`);
|
||||
userChoice = await getUserChoice("\nChoose an action:", [
|
||||
"Quit",
|
||||
"Print (create container, print DATABASE_URL)",
|
||||
"Replace (update DATABASE_URL in .env)",
|
||||
]);
|
||||
|
||||
if (userChoice === "1") {
|
||||
console.log("Exiting...");
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
console.log("No existing DATABASE_URL found");
|
||||
|
||||
// Ask what to do when no .env file or DATABASE_URL exists
|
||||
if (!existsSync(envPath)) {
|
||||
userChoice = await getUserChoice(
|
||||
"\nNo .env file found. What would you like to do?",
|
||||
[
|
||||
"Print (create container, print DATABASE_URL)",
|
||||
"Create .env file and add DATABASE_URL",
|
||||
"Quit",
|
||||
]
|
||||
);
|
||||
|
||||
if (userChoice === "3") {
|
||||
console.log("Exiting...");
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
console.log("Will add DATABASE_URL to existing .env file");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if container exists
|
||||
console.log("Checking for existing container...");
|
||||
const containerExists =
|
||||
await $`docker ps -a --filter name=${CONTAINER_NAME} --format "{{.Names}}"`
|
||||
.text()
|
||||
.then((names) => names.trim() === CONTAINER_NAME)
|
||||
.catch(() => false);
|
||||
|
||||
let shouldReplaceContainer = false;
|
||||
|
||||
if (containerExists) {
|
||||
console.log("Container already exists");
|
||||
|
||||
// Always ask what to do if container exists
|
||||
const replaceChoice = await getUserChoice(
|
||||
"\nContainer exists. What would you like to do?",
|
||||
["Use existing container", "Replace container (remove and create new)"],
|
||||
1
|
||||
);
|
||||
shouldReplaceContainer = replaceChoice === "2";
|
||||
|
||||
if (shouldReplaceContainer) {
|
||||
console.log("Removing existing container...");
|
||||
await $`docker rm --force --volumes ${CONTAINER_NAME}`;
|
||||
|
||||
// Explicitly remove the named volume to ensure clean state
|
||||
console.log("Removing volume...");
|
||||
await $`docker volume rm ${VOLUME_NAME}`.catch(() => {
|
||||
console.log("Volume doesn't exist or already removed");
|
||||
});
|
||||
} else {
|
||||
console.log("Using existing container");
|
||||
}
|
||||
}
|
||||
|
||||
// Create container if needed
|
||||
if (!containerExists || shouldReplaceContainer) {
|
||||
console.log("Creating PostgreSQL container...");
|
||||
await $`docker run --detach --name ${CONTAINER_NAME} --publish 5432:5432 --volume ${VOLUME_NAME}:/var/lib/postgresql/data --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=postgres --env POSTGRES_DB=pacman-server postgres:17`;
|
||||
}
|
||||
|
||||
// Format DATABASE_URL
|
||||
const databaseUrl =
|
||||
"postgresql://postgres:postgres@127.0.0.1:5432/pacman-server";
|
||||
|
||||
// Handle the final action based on user choice
|
||||
if (userChoice === "2") {
|
||||
// Print option
|
||||
console.log(`\nDATABASE_URL=${databaseUrl}`);
|
||||
} else if (
|
||||
userChoice === "3" ||
|
||||
(databaseUrlLine === -1 && userChoice === "2")
|
||||
) {
|
||||
// Replace or add to .env file
|
||||
if (databaseUrlLine !== -1) {
|
||||
// Replace existing line
|
||||
console.log("Updating DATABASE_URL in .env file...");
|
||||
envLines[databaseUrlLine] = `DATABASE_URL=${databaseUrl}`;
|
||||
writeFileSync(envPath, envLines.join("\n"));
|
||||
console.log("Updated .env file");
|
||||
} else {
|
||||
// Add new line
|
||||
console.log("Adding DATABASE_URL to .env file...");
|
||||
const newContent =
|
||||
envContent +
|
||||
(envContent.endsWith("\n") ? "" : "\n") +
|
||||
`DATABASE_URL=${databaseUrl}\n`;
|
||||
writeFileSync(envPath, newContent);
|
||||
console.log("Added to .env file");
|
||||
}
|
||||
}
|
||||
5172
Cargo.lock
generated
91
Cargo.toml
@@ -1,10 +1,10 @@
|
||||
[workspace]
|
||||
members = ["pacman", "pacman-common", "pacman-server"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
[package]
|
||||
name = "pacman"
|
||||
version = "0.2.0"
|
||||
authors = ["Xevion"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86.0"
|
||||
description = "A cross-platform retro Pac-Man clone, written in Rust and supported by SDL2"
|
||||
readme = true
|
||||
homepage = "https://pacman.xevion.dev"
|
||||
repository = "https://github.com/Xevion/Pac-Man"
|
||||
@@ -12,13 +12,66 @@ license = "GPL-3.0-or-later"
|
||||
keywords = ["game", "pacman", "arcade", "sdl2"]
|
||||
categories = ["games", "emulators"]
|
||||
publish = false
|
||||
exclude = ["/assets/unpacked/**", "/assets/site/**", "/bacon.toml", "/Justfile"]
|
||||
default-run = "pacman"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
# Improve build times by optimizing sqlx-macros
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
[dependencies]
|
||||
bevy_ecs = "0.16.1"
|
||||
glam = "0.30.5"
|
||||
pathfinding = "4.14"
|
||||
tracing = { version = "0.1.41", features = ["max_level_debug", "release_max_level_debug"]}
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = {version = "0.3.20", features = ["env-filter"]}
|
||||
time = { version = "0.3.43", features = ["formatting", "macros"] }
|
||||
thiserror = "2.0.16"
|
||||
anyhow = "1.0"
|
||||
smallvec = "1.15.1"
|
||||
bitflags = "2.9.4"
|
||||
micromap = "0.1.0"
|
||||
circular-buffer = "1.1.0"
|
||||
parking_lot = "0.12.3"
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.27.2"
|
||||
thousands = "0.2.0"
|
||||
num-width = "0.1.0"
|
||||
# While not actively used in code, `build.rs` generates code that relies on this. Keep the versions synchronized.
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
|
||||
# Windows-specific dependencies
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# Used for customizing console output on Windows; both are required due to the `windows` crate having poor Result handling with `GetStdHandle`.
|
||||
windows = { version = "0.61.3", features = ["Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console"] }
|
||||
windows-sys = { version = "0.60.2", features = ["Win32_System_Console"] }
|
||||
|
||||
# Desktop-specific dependencies
|
||||
[target.'cfg(not(target_os = "emscripten"))'.dependencies]
|
||||
# On desktop platforms, build SDL2 with cargo-vcpkg
|
||||
sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures", "static-link", "use-vcpkg"] }
|
||||
rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] }
|
||||
spin_sleep = "1.3.2"
|
||||
|
||||
# Browser-specific dependencies
|
||||
[target.'cfg(target_os = "emscripten")'.dependencies]
|
||||
# On Emscripten, we don't use cargo-vcpkg
|
||||
sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures"] }
|
||||
# TODO: Document why Emscripten cannot use `os_rng`.
|
||||
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
|
||||
libc = "0.2.175" # TODO: Describe why this is required.
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
speculoos = "0.13.0"
|
||||
|
||||
[build-dependencies]
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
|
||||
# phf generates runtime code which machete will not detect
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["phf"]
|
||||
|
||||
# Release profile for profiling (essentially the default 'release' profile with debug enabled)
|
||||
[profile.profile]
|
||||
@@ -27,8 +80,7 @@ debug = true
|
||||
# Undo the customizations for our release profile
|
||||
opt-level = 3
|
||||
lto = false
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
panic = 'unwind'
|
||||
|
||||
# Optimized release profile for size
|
||||
[profile.release]
|
||||
@@ -36,8 +88,13 @@ opt-level = "z"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
|
||||
# This profile is intended to appear as a 'release' profile to the build system due to`debug_assertions = false`,
|
||||
# but it will compile faster without optimizations. Useful for rapid testing of release-mode logic.
|
||||
[profile.dev-release]
|
||||
inherits = "dev"
|
||||
debug-assertions = false
|
||||
[package.metadata.vcpkg]
|
||||
dependencies = ["sdl2", "sdl2-image", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"]
|
||||
git = "https://github.com/microsoft/vcpkg"
|
||||
rev = "2024.05.24" # to check for a new one, check https://github.com/microsoft/vcpkg/releases
|
||||
|
||||
[package.metadata.vcpkg.target]
|
||||
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
|
||||
x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
|
||||
x86_64-apple-darwin = { triplet = "x64-osx" }
|
||||
aarch64-apple-darwin = { triplet = "arm64-osx" }
|
||||
|
||||
92
Justfile
@@ -1,79 +1,45 @@
|
||||
set shell := ["bash", "-c"]
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
|
||||
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
||||
# You can use src\\\\..., but the filename alone is acceptable too
|
||||
coverage_exclude_pattern := "src\\\\app\\.rs|audio\\.rs|src\\\\error\\.rs|platform\\\\emscripten\\.rs|bin\\\\.+\\.rs|main\\.rs|platform\\\\desktop\\.rs|platform\\\\tracing_buffer\\.rs|platform\\\\buffered_writer\\.rs|systems\\\\debug\\.rs|systems\\\\profiling\\.rs"
|
||||
|
||||
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
||||
|
||||
# Display available recipes
|
||||
default:
|
||||
just --list
|
||||
# !!! --ignore-filename-regex should be used on both reports & coverage testing
|
||||
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
|
||||
|
||||
# Open HTML coverage report
|
||||
# Generate HTML report (for humans, source line inspection)
|
||||
html: coverage
|
||||
cargo llvm-cov report \
|
||||
# prevents the absolute path from being used in the generated report
|
||||
--remap-path-prefix \
|
||||
--html \
|
||||
--open
|
||||
cargo llvm-cov report \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||
--html \
|
||||
--open
|
||||
|
||||
# Display coverage report
|
||||
# Display report (for humans)
|
||||
report-coverage: coverage
|
||||
cargo llvm-cov report --remap-path-prefix
|
||||
cargo llvm-cov report \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
||||
|
||||
# Generate baseline LCOV report
|
||||
# Run & generate report (for CI)
|
||||
coverage:
|
||||
cargo +nightly llvm-cov \
|
||||
--lcov \
|
||||
--remap-path-prefix \
|
||||
--workspace \
|
||||
--output-path lcov.info \
|
||||
--profile coverage \
|
||||
--no-fail-fast nextest
|
||||
cargo llvm-cov \
|
||||
--lcov \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||
--output-path lcov.info \
|
||||
--profile coverage \
|
||||
--no-fail-fast nextest
|
||||
|
||||
# Profile the project using samply
|
||||
# Profile the project using 'samply'
|
||||
samply:
|
||||
cargo build --profile profile
|
||||
samply record ./target/profile/pacman{{ binary_extension }}
|
||||
cargo build --profile profile
|
||||
samply record ./target/profile/pacman{{ binary_extension }}
|
||||
|
||||
# Build the project for Emscripten
|
||||
web *args:
|
||||
bun run pacman/web.build.ts {{args}}
|
||||
bun run --cwd web build
|
||||
caddy file-server --root web/dist/client
|
||||
|
||||
# Fix linting errors & formatting
|
||||
fix:
|
||||
cargo fix --workspace --lib --allow-dirty
|
||||
cargo fmt --all
|
||||
|
||||
# Push commits & tags
|
||||
push:
|
||||
git push origin --tags;
|
||||
git push
|
||||
|
||||
# Create a postgres container for the server
|
||||
server-postgres:
|
||||
bun run .scripts/postgres.ts
|
||||
|
||||
# Build the server image
|
||||
server-image:
|
||||
# build the server image
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
--file ./pacman-server/Dockerfile \
|
||||
--tag pacman-server \
|
||||
.
|
||||
|
||||
# Build and run the server in a Docker container
|
||||
run-server: server-image
|
||||
# remove the server container if it exists
|
||||
docker rm --force --volumes pacman-server
|
||||
|
||||
# run the server container
|
||||
docker run \
|
||||
--rm \
|
||||
--stop-timeout 2 \
|
||||
--name pacman-server \
|
||||
--publish 3000:3000 \
|
||||
--env PORT=3000 \
|
||||
--env-file pacman-server/.env \
|
||||
pacman-server
|
||||
bun run web.build.ts {{args}};
|
||||
caddy file-server --root dist
|
||||
|
||||
120
README.md
@@ -1,95 +1,80 @@
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
|
||||
<div align="center">
|
||||
<img src="assets/banner.png" alt="Pac-Man Banner Screenshot">
|
||||
</div>
|
||||
|
||||
# Pac-Man
|
||||
|
||||
[![A project just for fun, no really!][badge-justforfunnoreally]][justforfunnoreally] ![Built with Rust][badge-built-with-rust] [![Build Status][badge-build]][build] [![Tests Status][badge-test]][test] [![Checks Status][badge-checks]][checks] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo]
|
||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||
|
||||
[badge-built-with-rust]: https://img.shields.io/badge/Built_with-Rust-blue?logo=rust
|
||||
[badge-justforfunnoreally]: https://img.shields.io/badge/justforfunnoreally-dev-9ff
|
||||
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
||||
[badge-checks]: https://github.com/Xevion/Pac-Man/actions/workflows/checks.yaml/badge.svg
|
||||
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
||||
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
|
||||
[badge-online-demo]: https://img.shields.io/badge/Online%20Demo-Click%20Me!-brightgreen
|
||||
[justforfunnoreally]: https://justforfunnoreally.dev
|
||||
[badge-coverage]: https://codecov.io/github/Xevion/Pac-Man/branch/master/graph/badge.svg?token=R2RBYUQK3I
|
||||
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
||||
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
||||
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
||||
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
||||
[checks]: https://github.com/Xevion/Pac-Man/actions/workflows/checks.yaml
|
||||
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
|
||||
[coverage]: https://codecov.io/github/Xevion/Pac-Man
|
||||
[demo]: https://xevion.github.io/Pac-Man/
|
||||
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
||||
|
||||
A faithful recreation of the classic Pac-Man arcade game, written in Rust.
|
||||
|
||||
This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
|
||||
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
|
||||
|
||||
The game includes all the original features you'd expect from Pac-Man:
|
||||
|
||||
- [x] Classic maze navigation with tunnels and dot collection
|
||||
- [x] Classic maze navigation and dot collection
|
||||
- [ ] Four ghosts with their unique AI behaviors (Blinky, Pinky, Inky, and Clyde)
|
||||
- [x] Power pellets that allow Pac-Man to eat ghosts
|
||||
- [x] Fruit bonuses that appear periodically
|
||||
- [ ] Power pellets that allow Pac-Man to eat ghosts
|
||||
- [ ] Fruit bonuses that appear periodically
|
||||
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
|
||||
- [x] Authentic sound effects and sprites
|
||||
|
||||
This cross-platform implementation is built with SDL2 for graphics, audio, and input handling. It can run on Windows, Linux, macOS, even web browsers via WebAssembly.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The easiest way to play is to visit the [online demo][demo]. It is more or less identical to the desktop experience at this time.
|
||||
|
||||
While I do plan to have desktop builds released automatically, the game is still a work in progress, and I'm not quite ready to start uploading releases.
|
||||
|
||||
However, every commit has build artifacts, so you can grab the [latest build artifacts][build-workflow] if available.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div align="center">
|
||||
<img src="assets/screenshots/0.png" alt="Screenshot 0 - Starting Game">
|
||||
<p><em>Starting a new game</em></p>
|
||||
|
||||
<img src="assets/screenshots/1.png" alt="Screenshot 1 - Eating Dots">
|
||||
<p><em>Pac-Man collecting dots and avoiding ghosts</em></p>
|
||||
|
||||
<img src="assets/screenshots/2.png" alt="Screenshot 2 - Game Over">
|
||||
<p><em>Game over screen after losing all lives</em></p>
|
||||
|
||||
<img src="assets/screenshots/3.png" alt="Screenshot 3 - Debug Mode">
|
||||
<p><em>Debug mode showing hitboxes, node graph, and performance details.</em></p>
|
||||
</div>
|
||||
This cross-platform implementation is built with SDL2 for graphics, audio, and input handling. It can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
|
||||
|
||||
## Why?
|
||||
|
||||
[Just for fun.][justforfunnoreally] And because I wanted to learn more about Rust, inter-operability with C, and compiling to WebAssembly.
|
||||
Just because. And because I wanted to learn more about Rust, inter-operability with C, and compiling to WebAssembly.
|
||||
|
||||
Originally, I was inspired by a certain code review video on YouTube; [SOME UNIQUE C++ CODE // Pacman Clone Code Review](https://www.youtube.com/watch?v=OKs_JewEeOo). For some reason, I was inspired to try and replicate it in Rust, and it was uniquely challenging. It's not easy to integrate SDL2 with Rust, and even harder to get it working with Emscripten.
|
||||
I was inspired by a certain code review video on YouTube; [SOME UNIQUE C++ CODE // Pacman Clone Code Review](https://www.youtube.com/watch?v=OKs_JewEeOo) by The Cherno.
|
||||
|
||||
I wanted to hit a lot of goals and features, making it a 'perfect' project that I could be proud of.
|
||||
For some reason, I was inspired to try and replicate it in Rust, and it was uniquely challenging.
|
||||
|
||||
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors. No hacks, workarounds, or poor designs. Well documented, well-tested, and maintainable.
|
||||
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly. Statically linked, no runtime dependencies, automatically built with GitHub Actions.
|
||||
I wanted to hit a log of goals and features, making it a 'perfect' project that I could be proud of.
|
||||
|
||||
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors. No hacks, workarounds, or poor designs.
|
||||
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly. Statically linked, no runtime dependencies.
|
||||
- Performant, low memory, CPU and GPU usage.
|
||||
- Online demo, playable in a browser, built automatically with GitHub Actions.
|
||||
- Online demo, playable in a browser.
|
||||
- Completely automatic build system with releases for all platforms.
|
||||
- Well documented, well-tested, and maintainable.
|
||||
|
||||
If you're curious about the journey of this project, you can read the [story](STORY.md) file. Eventually, I will be using this as the basis for some sort of blog post or more official page, but for now, I'm keeping it within the repository as a simple file.
|
||||
## Experimental Ideas
|
||||
|
||||
## Roadmap
|
||||
|
||||
You can read the [roadmap](ROADMAP.md) file for more details on the project's goals and future plans.
|
||||
- Debug tooling
|
||||
- Game state visualization
|
||||
- Game speed controls + pausing
|
||||
- Log tracing
|
||||
- Performance details
|
||||
- Customized Themes & Colors
|
||||
- Color-blind friendly
|
||||
- Perfected Ghost Algorithms
|
||||
- More than 4 ghosts
|
||||
- Custom Level Generation
|
||||
- Multi-map tunnelling
|
||||
- Online Scoreboard
|
||||
- An online axum server with a simple database and OAuth2 authentication.
|
||||
- Integrates with GitHub, Discord, and Google OAuth2 to acquire an email identifier & avatar.
|
||||
- Avatars are optional for score submission and can be disabled, instead using a blank avatar.
|
||||
- Avatars are downscaled to a low resolution pixellated image to maintain the 8-bit aesthetic.
|
||||
- A custom name is used for the score submission, which is checked for potential abusive language.
|
||||
- A max length of 14 characters, and a min length of 3 characters.
|
||||
- Names are checked for potential abusive language via an external API.
|
||||
- The client implementation should require zero configuration, environment variables, or special secrets.
|
||||
- It simply defaults to the pacman server API, or can be overriden manually.
|
||||
|
||||
## Build Notes
|
||||
|
||||
Since this project is still in progress, I'm only going to cover non-obvious build details. By reading the code, build scripts, and copying the online build workflows, you should be able to replicate the build process.
|
||||
|
||||
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build --manifest-path pacman/Cargo.toml` to build the requisite dependencies via vcpkg.
|
||||
- `--manifest-path` is only required if you run it from the root directory; you can omit it if you `cd` into the `pacman` directory first.
|
||||
- This is only required for the desktop builds, not the web build.
|
||||
- We use rustc 1.86.0 for the build, due to bulk-memory-opt related issues on wasm32-unknown-emscripten.
|
||||
- Technically, we could probably use stable or even nightly on desktop targets, but using different versions for different targets is a pain, mainly because of clippy warnings changing between versions.
|
||||
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg.
|
||||
- For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git`
|
||||
- The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead.
|
||||
- I'm still not sure _why_ 3.1.43 is required, but it is. Perhaps in the future I will attempt to use a more modern version.
|
||||
@@ -102,18 +87,3 @@ Since this project is still in progress, I'm only going to cover non-obvious bui
|
||||
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))
|
||||
- `web.build.ts` auto installs dependencies, but you may need to pass `-i` or `--install=fallback|force` to install missing packages. My guess is that if you have some packages installed, it won't install any missing ones. If you have no packages installed, it will install all of them.
|
||||
- If you want to have TypeScript resolution for development, you can manually install the dependencies with `bun install` in the `assets/site` folder.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a pull request or open an issue.
|
||||
|
||||
- The code is not exactly stable or bulletproof, but it is functional and has a lot of tests.
|
||||
- I am not actively looking for contributors, but I will review pull requests and merge them if they are useful.
|
||||
- If you have any ideas, please feel free to submit an issue.
|
||||
- If you have any private issues, security concerns, or anything sensitive, you can email me at [xevion@xevion.dev](mailto:xevion@xevion.dev).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GPLv3 license. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
[build-workflow]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
||||
|
||||
164
ROADMAP.md
@@ -1,164 +0,0 @@
|
||||
# Roadmap
|
||||
|
||||
A comprehensive list of features needed to complete the Pac-Man emulation, organized by priority and implementation complexity.
|
||||
|
||||
## Core Game Features
|
||||
|
||||
### Ghost AI & Behavior
|
||||
|
||||
- [x] Core Ghost System Architecture
|
||||
- [x] Ghost entity types (Blinky, Pinky, Inky, Clyde)
|
||||
- [x] Ghost state management (Normal, Frightened, Eyes)
|
||||
- [x] Ghost movement and pathfinding systems
|
||||
- [ ] Authentic Ghost AI Personalities
|
||||
- [ ] Blinky (Red): Direct chase behavior
|
||||
- [ ] Pinky (Pink): Target 4 tiles ahead of Pac-Man
|
||||
- [ ] Inky (Cyan): Complex behavior based on Blinky's position
|
||||
- [ ] Clyde (Orange): Chase when far, flee when close
|
||||
- [x] Mode Switching System
|
||||
- [ ] Scatter/Chase pattern with proper timing
|
||||
- [x] Frightened mode transitions
|
||||
- [ ] Ghost house entry/exit mechanics
|
||||
- [x] Ghost House Behavior
|
||||
- [x] Proper spawning sequence
|
||||
- [ ] Exit timing and patterns
|
||||
- [ ] House-specific movement rules
|
||||
|
||||
### Fruit Bonus System
|
||||
|
||||
- [x] Fruit Spawning Mechanics
|
||||
- [x] Spawn at pellet counts 70 and 170
|
||||
- [x] Fruit display in bottom-right corner
|
||||
- [x] Fruit collection and scoring
|
||||
- [x] Bonus point display system
|
||||
|
||||
### Level Progression
|
||||
|
||||
- [ ] Multiple Levels
|
||||
- [ ] Level completion detection
|
||||
- [ ] Progressive difficulty scaling
|
||||
- [ ] Ghost speed increases per level
|
||||
- [ ] Power pellet duration decreases
|
||||
- [ ] Intermission Screens
|
||||
- [ ] Between-level cutscenes
|
||||
- [ ] Proper graphics and timing
|
||||
|
||||
### Audio System Completion
|
||||
|
||||
- [x] Core Audio Infrastructure
|
||||
- [x] Audio event system
|
||||
- [x] Sound effect playback
|
||||
- [x] Audio muting controls
|
||||
- [ ] Background Music
|
||||
- [x] Intro jingle
|
||||
- [ ] Continuous gameplay music
|
||||
- [ ] Escalating siren based on remaining pellets
|
||||
- [ ] Power pellet mode music
|
||||
- [ ] Intermission music
|
||||
- [x] Sound Effects
|
||||
- [x] Pellet eating sounds
|
||||
- [x] Fruit collection sounds
|
||||
- [x] Ghost eaten sounds
|
||||
- [x] Pac-Man Death
|
||||
- [ ] Ghost movement sounds
|
||||
- [ ] Level completion fanfare
|
||||
|
||||
### Game Mechanics
|
||||
|
||||
- [ ] Bonus Lives
|
||||
- [ ] Extra life at 10,000 points
|
||||
- [x] Life counter display
|
||||
- [ ] High Score System
|
||||
- [ ] High score tracking
|
||||
- [x] High score display
|
||||
- [ ] Score persistence
|
||||
|
||||
## Secondary Features (Medium Priority)
|
||||
|
||||
### Game Polish
|
||||
|
||||
- [x] Core Input System
|
||||
- [x] Keyboard controls
|
||||
- [x] Direction buffering for responsive controls
|
||||
- [x] Touch controls for mobile
|
||||
- [x] Pause System
|
||||
- [x] Pause/unpause functionality
|
||||
- [ ] Pause menu with options
|
||||
- [ ] Input System
|
||||
- [ ] Input remapping
|
||||
- [ ] Multiple input methods
|
||||
|
||||
## Advanced Features (Lower Priority)
|
||||
|
||||
### Difficulty Options
|
||||
|
||||
- [ ] Easy/Normal/Hard modes
|
||||
- [ ] Customizable ghost speeds
|
||||
|
||||
### Data Persistence
|
||||
|
||||
- [ ] High Score Persistence
|
||||
- [ ] Save high scores to file
|
||||
- [ ] High score table display
|
||||
- [ ] Settings Storage
|
||||
- [ ] Save user preferences
|
||||
- [ ] Audio/visual settings
|
||||
- [ ] Statistics Tracking
|
||||
- [ ] Game statistics
|
||||
- [ ] Achievement system
|
||||
|
||||
### Debug & Development Tools
|
||||
|
||||
- [x] Performance details
|
||||
- [x] Core Debug Infrastructure
|
||||
- [x] Debug mode toggle
|
||||
- [x] Comprehensive game event logging
|
||||
- [x] Performance profiling tools
|
||||
- [ ] Game State Visualization
|
||||
- [ ] Ghost AI state display
|
||||
- [ ] Pathfinding visualization
|
||||
- [ ] Collision detection display
|
||||
- [ ] Game Speed Controls
|
||||
- [ ] Variable game speed for testing
|
||||
- [ ] Frame-by-frame stepping
|
||||
|
||||
## Customization & Extensions
|
||||
|
||||
### Visual Customization
|
||||
|
||||
- [x] Core Rendering System
|
||||
- [x] Sprite-based rendering
|
||||
- [x] Layered rendering system
|
||||
- [x] Animation system
|
||||
- [x] HUD rendering
|
||||
- [ ] Display Options
|
||||
- [x] Fullscreen support
|
||||
- [x] Window resizing
|
||||
- [ ] Pause while resizing (SDL2 limitation mitigation)
|
||||
- [ ] Multiple resolution support
|
||||
|
||||
### Gameplay Extensions
|
||||
|
||||
- [ ] Advanced Ghost AI
|
||||
- [ ] Support for >4 ghosts
|
||||
- [ ] Custom ghost behaviors
|
||||
- [ ] Level Generation
|
||||
- [ ] Custom level creation
|
||||
- [ ] Multi-map tunneling
|
||||
- [ ] Level editor
|
||||
|
||||
## Online Features (Future)
|
||||
|
||||
### Scoreboard System
|
||||
|
||||
- [ ] Backend Infrastructure
|
||||
- [ ] Axum server with database
|
||||
- [ ] OAuth2 authentication
|
||||
- [ ] GitHub/Discord/Google auth
|
||||
- [ ] Profile Features
|
||||
- [ ] Optional avatars (8-bit aesthetic)
|
||||
- [ ] Custom names (3-14 chars, filtered)
|
||||
- [ ] Client Implementation
|
||||
- [ ] Zero-config client
|
||||
- [ ] Default API endpoint
|
||||
- [ ] Manual override available
|
||||
8
STORY.md
@@ -31,7 +31,7 @@ WebAssembly.
|
||||
The problem is that much of this work was done for pure-Rust applications - and SDL is C++.
|
||||
This requires a C++ WebAssembly compiler such as Emscripten; and it's a pain to get working.
|
||||
|
||||
Luckily though, someone else has done this before, and they fully documented it - [RuggRouge][ruggrogue].
|
||||
Luckily though, someone else has done this before, and they fully documented it - [RuggRouge][ruggrouge].
|
||||
|
||||
- Built with Rust
|
||||
- Uses SDL2
|
||||
@@ -92,7 +92,7 @@ This was weird, and honestly, I'm confused as to why the 2-year old sample code
|
||||
|
||||
After a bit of time, I noted that the `Instant` times were printing with only the whole seconds changing, and the nanoseconds were always 0.
|
||||
|
||||
```rust
|
||||
```
|
||||
Instant { tv_sec: 0, tv_nsec: 0 }
|
||||
Instant { tv_sec: 1, tv_nsec: 0 }
|
||||
Instant { tv_sec: 2, tv_nsec: 0 }
|
||||
@@ -357,7 +357,7 @@ Doing so required a full re-work of the animation and texture system, and I ende
|
||||
|
||||
So, I ended up using `unsafe` to forcibly cast the lifetimes to `'static`, which was a bit of a gamble, but given that they essentially behave as `'static` in practice, there wasn't much risk as I see it. I might re-look into my understanding of lifetimes and this in the future, but for the time being, it's a good solution that makes the codebase far easier to work with.
|
||||
|
||||
## Implementing Cross-platform Builds for Pac-Man
|
||||
## Cross-platform Builds
|
||||
|
||||
Since the original `rust-sdl2-emscripten` demo project had cross-platform builds, I was ready to get it working for this project. For the most part, it wasn't hard, things tended to click into place, but unfortunately, the `emscripten` os target and somehow, the `linux` os target were both failing.
|
||||
|
||||
@@ -412,8 +412,8 @@ The bigger downside was that I had to toss out almost all the existing code for
|
||||
|
||||
This ended up being okay though, as I was able to clean up a lot of gross code, and the system ended up being easier to work with by comparison.
|
||||
|
||||
[code-review-thumbnail]: https://img.youtube.com/vi/OKs_JewEeOo/hqdefault.jpg
|
||||
[code-review-video]: https://www.youtube.com/watch?v=OKs_JewEeOo
|
||||
[code-review-thumbnail]: https://img.youtube.com/vi/OKs_JewEeOo/hqdefault.jpg
|
||||
[fighting-lifetimes-1]: https://devcry.heiho.net/html/2022/20220709-rust-and-sdl2-fighting-with-lifetimes.html
|
||||
[fighting-lifetimes-2]: https://devcry.heiho.net/html/2022/20220716-rust-and-sdl2-fighting-with-lifetimes-2.html
|
||||
[fighting-lifetimes-3]: https://devcry.heiho.net/html/2022/20220724-rust-and-sdl2-fighting-with-lifetimes-3.html
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
BIN
assets/game/sound/waka/1.ogg
Normal file
BIN
assets/game/sound/waka/2.ogg
Normal file
BIN
assets/game/sound/waka/3.ogg
Normal file
BIN
assets/game/sound/waka/4.ogg
Normal file
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 120 B After Width: | Height: | Size: 120 B |
|
Before Width: | Height: | Size: 120 B After Width: | Height: | Size: 120 B |
|
Before Width: | Height: | Size: 116 B After Width: | Height: | Size: 116 B |
|
Before Width: | Height: | Size: 115 B After Width: | Height: | Size: 115 B |
|
Before Width: | Height: | Size: 192 B After Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 187 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 215 B |
|
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 189 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 115 B After Width: | Height: | Size: 115 B |
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 125 B After Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 122 B After Width: | Height: | Size: 122 B |
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 173 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 213 B After Width: | Height: | Size: 213 B |
|
Before Width: | Height: | Size: 178 B After Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 162 B After Width: | Height: | Size: 162 B |
|
Before Width: | Height: | Size: 210 B After Width: | Height: | Size: 210 B |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 136 B After Width: | Height: | Size: 136 B |
|
Before Width: | Height: | Size: 137 B After Width: | Height: | Size: 137 B |
|
Before Width: | Height: | Size: 147 B After Width: | Height: | Size: 147 B |
|
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
|
Before Width: | Height: | Size: 148 B After Width: | Height: | Size: 148 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 152 B After Width: | Height: | Size: 152 B |
|
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 150 B |
|
Before Width: | Height: | Size: 151 B After Width: | Height: | Size: 151 B |
|
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
|
Before Width: | Height: | Size: 178 B After Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 189 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 187 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 194 B After Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 191 B After Width: | Height: | Size: 191 B |
|
Before Width: | Height: | Size: 194 B After Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 179 B After Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 127 B After Width: | Height: | Size: 127 B |
|
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 134 B After Width: | Height: | Size: 134 B |
|
Before Width: | Height: | Size: 134 B After Width: | Height: | Size: 134 B |
|
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 186 B |
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 180 B After Width: | Height: | Size: 180 B |
|
Before Width: | Height: | Size: 182 B After Width: | Height: | Size: 182 B |