Compare commits

..

15 Commits

Author SHA1 Message Date
Ethan O'Brien
6688655370 Add super cool Live Clear Rates webui 2025-12-24 21:10:37 -06:00
Ethan O'Brien
955e3819b5 Add music masterdata 2025-12-24 21:09:34 -06:00
Ethan O'Brien
591990a040 Track rage-quits as failed lives 2025-12-24 19:46:30 -06:00
Ethan O'Brien
0fc7f1d36e Redo migration table 2025-12-24 17:42:41 -06:00
Ethan O'Brien
15155bd8df Move migration related userdata functions to a new file 2025-12-24 14:34:13 -06:00
Ethan O'Brien
816589ae22 Fix deck size for new accounts 2025-12-24 14:18:31 -06:00
Ethan O'Brien
d14533d966 Tell workflow to checkout submodules 2025-12-24 13:21:49 -06:00
Ethan O'Brien
697f4188c2 Ship files for offline android/ios servers 2025-12-24 13:13:46 -06:00
Ethan O'Brien
01f3a42613 Add more targets when build 2025-12-03 20:58:07 -06:00
Ethan O'Brien
6d541a81ef Add actions/checkout 2025-11-30 18:29:09 -06:00
Ethan O'Brien
b9f9929ea9 Print version to stdout 2025-11-30 18:27:30 -06:00
Ethan O'Brien
2cc2138eda Update docker build to push version tags 2025-11-30 18:16:03 -06:00
Ethan O'Brien
bab9868355 Update license 2025-11-30 18:09:47 -06:00
Ethan O'Brien
ad0d222c96 Add the (currently android) easter mode 2025-11-30 17:26:26 -06:00
Ethan O'Brien
60eb7d469b Add ability to set datapath outside of cli args 2025-11-30 16:51:08 -06:00
28 changed files with 26325 additions and 256 deletions

View File

@@ -8,6 +8,9 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checking out branch
uses: actions/checkout@v3
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
@@ -26,7 +29,7 @@ jobs:
- name: Extract version from Cargo.toml - name: Extract version from Cargo.toml
shell: bash shell: bash
run: | run: |
echo "EW_VERSION=$(grep "^version" Cargo.toml | sed -E 's/version\s*=\s*["]([^"]*)["]/\1/')" >> $GITHUB_ENV echo "APP_VERSION=$(grep "^version" Cargo.toml | sed -E 's/version\s*=\s*["]([^"]*)["]/\1/')" >> $GITHUB_ENV
- name: Log in to DockerHub - name: Log in to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -52,10 +55,12 @@ jobs:
push: true push: true
labels: | labels: |
gitsha1=${{ github.sha }} gitsha1=${{ github.sha }}
org.opencontainers.image.version=${{ env.EW_VERSION }} org.opencontainers.image.version=${{ env.APP_VERSION }}
tags: "${{ steps.set-tag.outputs.tags }}" tags: |
${{ steps.set-tag.outputs.tags }}
docker.io/ethanaobrien/ew:${{ env.APP_VERSION }}
file: "docker/Dockerfile" file: "docker/Dockerfile"
platforms: linux/amd64 #,linux/arm64 platforms: linux/amd64, linux/arm64
# arm64 builds OOM without the git fetch setting. c.f. # arm64 builds OOM without the git fetch setting. c.f.
# https://github.com/rust-lang/cargo/issues/10583 # https://github.com/rust-lang/cargo/issues/10583

View File

@@ -2,7 +2,8 @@ name: Build release binaries
on: on:
push: push:
tags: 'v*' branches: '**'
tags: '*'
jobs: jobs:
build: build:
@@ -10,6 +11,8 @@ jobs:
steps: steps:
- name: Checking out branch - name: Checking out branch
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust - name: Install Rust
shell: bash shell: bash
@@ -20,6 +23,12 @@ jobs:
with: with:
node-version: '18' node-version: '18'
- name: Install android NDK
id: setup-ndk
uses: https://github.com/nttld/setup-ndk@v1
with:
ndk-version: r29
- name: Create out directory - name: Create out directory
run: | run: |
mkdir out mkdir out
@@ -30,21 +39,75 @@ jobs:
npm install npm install
npm run build npm run build
- name: Build x86_64-unknown-linux-gnu - name: Install cargo ndk
shell: bash
run: | run: |
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
cargo build --target x86_64-unknown-linux-gnu --release || echo "Failed to build" cargo install cargo-ndk
mv target/x86_64-unknown-linux-gnu/release/ew out/ew-x86_64-unknown-linux-gnu rustup target add aarch64-linux-android
- name: Build x86_64-pc-windows-gnu - name: Write cargo linker config
shell: bash
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
mkdir -p .cargo
echo "[target.aarch64-linux-android]" > .cargo/config.toml
echo "linker = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang\"" >> .cargo/config.toml
echo "ar = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"" >> .cargo/config.toml
echo "" >> .cargo/config.toml
echo "[target.aarch64-unknown-linux-gnu]" >> .cargo/config.toml
echo "linker = \"aarch64-linux-gnu-gcc\"" >> .cargo/config.toml
echo "ar = \"aarch64-linux-gnu-ar\"" >> .cargo/config.toml
echo "" >> .cargo/config.toml
echo "[target.x86_64-linux-android]" >> .cargo/config.toml
echo "linker = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang\"" >> .cargo/config.toml
echo "ar = \"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"" >> .cargo/config.toml
- name: Build Stuff
shell: bash
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: | run: |
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
sudo apt update && sudo apt install gcc-mingw-w64 -y export PATH=$PATH:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/
rustup target add x86_64-pc-windows-gnu sudo apt update && sudo apt install gcc-mingw-w64 gcc-aarch64-linux-gnu -y
cargo build --target x86_64-pc-windows-gnu --release || echo "Failed to build"
mv target/x86_64-pc-windows-gnu/release/ew.exe out/ew-x86_64-pc-windows-gnu.exe arr=("aarch64-linux-android" "aarch64-unknown-linux-gnu" "x86_64-linux-android" "x86_64-pc-windows-gnu" "x86_64-unknown-linux-gnu")
for target in "${arr[@]}"; do
rustup target add $target
cargo build --target $target --release || echo "Failed to build"
if [[ "$target" == *"windows"* ]]; then
mv target/$target/release/ew.exe out/ew-$target.exe
else
mv target/$target/release/ew out/ew-$target
fi
done
- name: Patch for library build
run: |
sed -i 's|#\[lib\]|\[lib\]|g' Cargo.toml
sed -i 's|#crate-type|crate-type|g' Cargo.toml
sed -i 's|#required-features|required-features|g' Cargo.toml
- name: Build jnilibs
shell: bash
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
. "$HOME/.cargo/env"
cargo ndk -t arm64-v8a build --release --features library
mv target/aarch64-linux-android/release/libew.so out/libew-aarch64-linux-android.so
- name: Upload artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v3
with:
name: output
path: ./out/
- name: Publish release - name: Publish release
if: startsWith(github.ref, 'refs/tags/')
uses: actions/forgejo-release@v2 uses: actions/forgejo-release@v2
with: with:
direction: upload direction: upload

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "assets"]
path = assets
url = https://git.ethanthesleepy.one/ethanaobrien/sif2-runtime-files

101
Cargo.lock generated
View File

@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -132,9 +132,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web" name = "actix-web"
version = "4.12.0" version = "4.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2233f53f6cb18ae038ce1f0713ca0c72ca0c4b71fe9aaeb59924ce2c89c6dd85" checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@@ -182,7 +182,7 @@ dependencies = [
"actix-router", "actix-router",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -392,9 +392,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.46" version = "1.2.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -455,7 +455,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -577,7 +577,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"unicode-xid", "unicode-xid",
] ]
@@ -611,7 +611,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -869,12 +869,11 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "1.3.1" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv",
"itoa", "itoa",
] ]
@@ -1009,7 +1008,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"zstd", "zstd",
] ]
@@ -1119,9 +1118,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.82" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -1719,9 +1718,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [ dependencies = [
"zeroize", "zeroize",
] ]
@@ -1791,7 +1790,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -1849,9 +1848,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -1959,9 +1958,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.110" version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1976,7 +1975,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -1996,7 +1995,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2071,9 +2070,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.41" version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
@@ -2083,20 +2082,20 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.30" version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.34" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
] ]
@@ -2144,12 +2143,12 @@ dependencies = [
[[package]] [[package]]
name = "ureq-proto" name = "ureq-proto"
version = "0.5.2" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
dependencies = [ dependencies = [
"base64", "base64",
"http 1.3.1", "http 1.4.0",
"httparse", "httparse",
"log", "log",
] ]
@@ -2240,9 +2239,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -2253,9 +2252,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2263,22 +2262,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2582,28 +2581,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"synstructure", "synstructure",
] ]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.28" version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.28" version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2623,7 +2622,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"synstructure", "synstructure",
] ]
@@ -2663,7 +2662,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]

View File

@@ -30,18 +30,18 @@ ureq = "3.1.4"
include_dir = {version = "0.7.4", optional = true } include_dir = {version = "0.7.4", optional = true }
[target.aarch64-linux-android.dependencies] [target.aarch64-linux-android.dependencies]
jni = { version = "0.21.1", features = ["invocation", "default"] } jni = { version = "0.21.1", features = ["invocation", "default"], optional = true }
[target.aarch64-apple-ios.dependencies] [target.aarch64-apple-ios.dependencies]
objc2 = "0.6.3" objc2 = { version = "0.6.3", optional = true }
objc2-foundation = { version = "0.3.2", features = ["NSFileManager"] } objc2-foundation = { version = "0.3.2", features = ["NSFileManager"], optional = true }
[build-dependencies] [build-dependencies]
cc = "1.0" cc = "1.0"
# To enable this library you MUST comment out lib block below and add --features library # To enable this library you MUST comment out lib block below and add --features library
[features] [features]
library = ["dep:include_dir"] library = ["jni", "objc2", "objc2-foundation", "include_dir"]
#[lib] #[lib]
#crate-type = ["cdylib"] #crate-type = ["cdylib"]

View File

@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author> ew Copyright (C) 2024-2025 Ethan O'Brien
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

1
assets Submodule

Submodule assets added at dbb068e72c

View File

@@ -33,10 +33,10 @@ extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_startServer<'l
data_path: JString<'local>, data_path: JString<'local>,
easter: jboolean easter: jboolean
) -> jstring { ) -> jstring {
//crate::runtime::set_easter_mode(easter != 0); crate::runtime::set_easter_mode(easter != 0);
let data_path: String = env.get_string(&data_path).unwrap().into(); let data_path: String = env.get_string(&data_path).unwrap().into();
//crate::runtime::set_datapath(data_path); crate::runtime::update_data_path(&data_path);
let output = env.new_string(String::from("Azunyannnn~")).unwrap(); let output = env.new_string(String::from("Azunyannnn~")).unwrap();
thread::spawn(|| { thread::spawn(|| {
@@ -56,5 +56,5 @@ extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_stopServer<'lo
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_setEasterMode<'local>(_env: JNIEnv<'local>, _class: JClass<'local>, easter: jboolean) { extern "C" fn Java_one_ethanthesleepy_androidew_BackgroundService_setEasterMode<'local>(_env: JNIEnv<'local>, _class: JClass<'local>, easter: jboolean) {
//crate::runtime::set_easter_mode(easter != 0); crate::runtime::set_easter_mode(easter != 0);
} }

View File

@@ -7,7 +7,7 @@ pub static INITIALIZER: extern "C" fn() = main;
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn main() { pub extern "C" fn main() {
let data_path = get_bundle_path().into_os_string().into_string().unwrap(); let data_path = get_bundle_path().into_os_string().into_string().unwrap();
//set_datapath(data_path); crate::runtime::update_data_path(&data_path);
std::thread::spawn(|| { std::thread::spawn(|| {
crate::run_server(true).unwrap(); crate::run_server(true).unwrap();
@@ -18,7 +18,7 @@ pub extern "C" fn main() {
use objc2_foundation::{NSFileManager, NSSearchPathDirectory, NSSearchPathDomainMask}; use objc2_foundation::{NSFileManager, NSSearchPathDirectory, NSSearchPathDomainMask};
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
fn get_bundle_path() -> std::path::PathBuf { pub fn get_bundle_path() -> std::path::PathBuf {
unsafe { unsafe {
let manager = NSFileManager::defaultManager(); let manager = NSFileManager::defaultManager();
let application_support = manager.URLsForDirectory_inDomains(NSSearchPathDirectory::ApplicationSupportDirectory, NSSearchPathDomainMask::UserDomainMask); let application_support = manager.URLsForDirectory_inDomains(NSSearchPathDirectory::ApplicationSupportDirectory, NSSearchPathDomainMask::UserDomainMask);

View File

@@ -3,7 +3,7 @@ mod options;
mod router; mod router;
mod encryption; mod encryption;
mod sql; mod sql;
mod runtime; pub mod runtime;
#[macro_use] #[macro_use]
mod macros; mod macros;
@@ -22,9 +22,9 @@ use actix_web::{
web, web,
dev::Service dev::Service
}; };
use std::fs;
use std::time::Duration; use std::time::Duration;
use options::get_args; pub use options::get_args;
use runtime::get_data_path;
#[actix_web::main] #[actix_web::main]
pub async fn run_server(in_thread: bool) -> std::io::Result<()> { pub async fn run_server(in_thread: bool) -> std::io::Result<()> {
@@ -39,7 +39,12 @@ pub async fn run_server(in_thread: bool) -> std::io::Result<()> {
let rv = HttpServer::new(|| App::new() let rv = HttpServer::new(|| App::new()
.wrap_fn(|req, srv| { .wrap_fn(|req, srv| {
println!("Request: {}", req.path()); println!("Request: {} {}", req.method(), req.path());
#[cfg(feature = "library")]
#[cfg(target_os = "android")]
log_to_logcat!("ew", "Request: {} {}", req.method(), req.path());
srv.call(req) srv.call(req)
}) })
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25)) .app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
@@ -78,13 +83,3 @@ pub async fn stop_server() {
runtime::set_running(false); runtime::set_running(false);
println!("Stopping"); println!("Stopping");
} }
pub fn get_data_path(file_name: &str) -> String {
let args = get_args();
let mut path = args.path;
while path.ends_with('/') {
path.pop();
}
fs::create_dir_all(&path).unwrap();
format!("{}/{}", path, file_name)
}

View File

@@ -1,6 +1,7 @@
#[cfg(not(feature = "library"))] #[cfg(not(feature = "library"))]
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
ew::runtime::update_data_path(&ew::get_args().path);
ew::run_server(false) ew::run_server(false)
} }

View File

@@ -170,7 +170,7 @@ async fn api_req(req: HttpRequest, body: String) -> HttpResponse {
pub async fn request(req: HttpRequest, body: String) -> HttpResponse { pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
let args = crate::get_args(); let args = crate::get_args();
let headers = req.headers(); let headers = req.headers();
if args.hidden && req.path().starts_with("/api/webui/") { if args.hidden && (req.path().starts_with("/api/webui/") || req.path().starts_with("/live_clear_rate.html")) {
return not_found(headers); return not_found(headers);
} }
if headers.get("aoharu-asset-version").is_none() && req.path().starts_with("/api") && !req.path().starts_with("/api/webui") { if headers.get("aoharu-asset-version").is_none() && req.path().starts_with("/api") && !req.path().starts_with("/api/webui") {
@@ -212,6 +212,7 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
"/v1.0/payment/balance" => gree::balance(req), "/v1.0/payment/balance" => gree::balance(req),
"/web/announcement" => web::announcement(req), "/web/announcement" => web::announcement(req),
"/api/webui/userInfo" => webui::user(req), "/api/webui/userInfo" => webui::user(req),
"/live_clear_rate.html" => clear_rate::clearrate_html(req).await,
"/webui/logout" => webui::logout(req), "/webui/logout" => webui::logout(req),
"/api/webui/export" => webui::export(req), "/api/webui/export" => webui::export(req),
"/api/webui/serverInfo" => webui::server_info(req), "/api/webui/serverInfo" => webui::server_info(req),

View File

@@ -1,5 +1,5 @@
use json::{object, array, JsonValue}; use json::{object, array, JsonValue};
use actix_web::{HttpRequest}; use actix_web::{HttpRequest, HttpResponse, http::header::ContentType};
use rusqlite::params; use rusqlite::params;
use std::sync::Mutex; use std::sync::Mutex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@@ -7,6 +7,7 @@ use lazy_static::lazy_static;
use crate::encryption; use crate::encryption;
use crate::sql::SQLite; use crate::sql::SQLite;
use crate::router::{global, databases}; use crate::router::{global, databases};
use crate::include_file;
trait SqlClearRate { trait SqlClearRate {
fn get_live_data(&self, id: i64) -> Result<Live, rusqlite::Error>; fn get_live_data(&self, id: i64) -> Result<Live, rusqlite::Error>;
@@ -34,6 +35,7 @@ impl SqlClearRate for SQLite {
lazy_static! { lazy_static! {
static ref DATABASE: SQLite = SQLite::new("live_statistics.db", setup_tables); static ref DATABASE: SQLite = SQLite::new("live_statistics.db", setup_tables);
static ref CACHED_DATA: Mutex<Option<JsonValue>> = Mutex::new(None); static ref CACHED_DATA: Mutex<Option<JsonValue>> = Mutex::new(None);
static ref CACHED_HTML_DATA: Mutex<Option<JsonValue>> = Mutex::new(None);
} }
pub struct Live { pub struct Live {
@@ -150,6 +152,18 @@ pub fn live_completed(id: i64, level: i32, failed: bool, score: i64, uid: i64) {
}; };
} }
fn get_song_title(live_id: i32, english: bool) -> String {
let details = if english {
databases::MUSIC_EN[live_id.to_string()].clone()
} else {
databases::MUSIC[live_id.to_string()].clone()
};
if !details.is_null() {
return details["name"].to_string();
}
String::from("Unknown Song")
}
fn get_pass_percent(failed: i64, pass: i64) -> String { fn get_pass_percent(failed: i64, pass: i64) -> String {
let total = (failed + pass) as f64; let total = (failed + pass) as f64;
if failed + pass == 0 { if failed + pass == 0 {
@@ -236,3 +250,101 @@ pub fn ranking(_req: HttpRequest, body: String) -> Option<JsonValue> {
"ranking_list": rank "ranking_list": rank
}) })
} }
fn get_html() -> JsonValue {
let lives = DATABASE.lock_and_select_all("SELECT live_id FROM lives", params!()).unwrap();
let mut table = String::new();
for id in lives.members() {
let live_id = id.as_i64().unwrap();
let info = match DATABASE.get_live_data(live_id) {
Ok(i) => i,
Err(_) => continue,
};
let calc_rate = |pass: i64, fail: i64| -> f64 {
let total = pass + fail;
if total == 0 { 0.0 } else { pass as f64 / total as f64 }
};
let title_jp = get_song_title(info.live_id, false);
let title_en = get_song_title(info.live_id, true);
let normal_txt = get_pass_percent(info.normal_failed, info.normal_pass);
let hard_txt = get_pass_percent(info.hard_failed, info.hard_pass);
let expert_txt = get_pass_percent(info.expert_failed, info.expert_pass);
let master_txt = get_pass_percent(info.master_failed, info.master_pass);
let normal_plays = info.normal_pass + info.normal_failed;
let hard_plays = info.hard_pass + info.hard_failed;
let expert_plays = info.expert_pass + info.expert_failed;
let master_plays = info.master_pass + info.master_failed;
let normal_rate_sort = calc_rate(info.normal_pass, info.normal_failed);
let hard_rate_sort = calc_rate(info.hard_pass, info.hard_failed);
let expert_rate_sort = calc_rate(info.expert_pass, info.expert_failed);
let master_rate_sort = calc_rate(info.master_pass, info.master_failed);
table.push_str(&format!(
r#"<tr>
<td class="title-cell"
data-val="{title_jp}"
data-title-en="{title_en}"
data-title-jp="{title_jp}">
{title_jp}
</td>
<td data-plays="{normal_plays}" data-rate="{normal_rate_sort}">
<span class="rate-text">{normal_txt}</span>
<span class="meta-text">{normal_plays} plays</span>
</td>
<td data-plays="{hard_plays}" data-rate="{hard_rate_sort}">
<span class="rate-text">{hard_txt}</span>
<span class="meta-text">{hard_plays} plays</span>
</td>
<td data-plays="{expert_plays}" data-rate="{expert_rate_sort}">
<span class="rate-text">{expert_txt}</span>
<span class="meta-text">{expert_plays} plays</span>
</td>
<td data-plays="{master_plays}" data-rate="{master_rate_sort}">
<span class="rate-text">{master_txt}</span>
<span class="meta-text">{master_plays} plays</span>
</td>
</tr>"#
));
}
let html = include_file!("src/router/clear_rate_template.html").replace("{{TABLEBODY}}", &table);
object!{
"cache": html,
"last_updated": global::timestamp()
}
}
async fn get_clearrate_html() -> String {
let cache = {
let mut result = crate::lock_onto_mutex!(CACHED_HTML_DATA);
if result.is_none() {
result.replace(get_html());
}
result.as_ref().unwrap().clone()
};
if cache["last_updated"].as_u64().unwrap() + (60 * 60) < global::timestamp() {
let mut result = crate::lock_onto_mutex!(CACHED_HTML_DATA);
result.replace(get_html());
}
cache["cache"].to_string()
}
pub async fn clearrate_html(_req: HttpRequest) -> HttpResponse {
let html = get_clearrate_html().await;
HttpResponse::Ok()
.content_type(ContentType::html())
.body(html)
}

View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Live Clear Rates</title>
<style>
:root {
--bg-color: #f4f4f9;
--text-color: #333;
--table-bg: #ffffff;
--primary-color: #6c5ce7;
--primary-hover: #5649c0;
--header-text: #ffffff;
--border-color: #dddddd;
--row-even: #f8f9fa;
--row-hover: #eef2ff;
--meta-text: #666;
--shadow: rgba(0, 0, 0, 0.1);
}
body.dark-mode {
--bg-color: #18181b;
--text-color: #e4e4e7;
--table-bg: #27272a;
--primary-color: #818cf8;
--primary-hover: #6366f1;
--header-text: #ffffff;
--border-color: #3f3f46;
--row-even: #2f2f35;
--row-hover: #353540;
--meta-text: #a1a1aa;
--shadow: rgba(0, 0, 0, 0.4);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
padding: 20px;
transition: background-color 0.3s, color 0.3s;
}
h1 { text-align: center; margin-bottom: 20px; }
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
border: 2px solid var(--primary-color);
background: transparent;
color: var(--text-color);
border-radius: 20px;
cursor: pointer;
font-weight: 600;
transition: 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover, .btn.active {
background-color: var(--primary-color);
color: var(--header-text);
}
.table-container {
overflow-x: auto;
box-shadow: 0 0 20px var(--shadow);
border-radius: 8px;
background: var(--table-bg);
}
table { width: 100%; border-collapse: collapse; min-width: 600px; }
th, td {
padding: 12px 15px;
text-align: center;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: var(--primary-color);
color: var(--header-text);
cursor: pointer;
user-select: none;
position: sticky;
top: 0;
white-space: nowrap;
}
th:hover { background-color: var(--primary-hover); }
tr:nth-of-type(even) { background-color: var(--row-even); }
tr:last-of-type { border-bottom: 2px solid var(--primary-color); }
tr:hover { background-color: var(--row-hover); transition: 0.2s; }
th::after { content: ' \2195'; opacity: 0.3; font-size: 0.8em; margin-left: 5px; }
th.asc::after { content: ' \2191'; opacity: 1; }
th.desc::after { content: ' \2193'; opacity: 1; }
.meta-text { font-size: 0.85em; color: var(--meta-text); display: block; margin-top: 4px; }
.rate-text { font-weight: bold; font-size: 1.1em; }
.title-cell { text-align: left; font-weight: 600; }
</style>
</head>
<body>
<h1>Live Clear Rates</h1>
<div class="controls">
<button class="btn" id="btnLang" onclick="toggleLanguage()">Language: JP</button>
<button class="btn active" id="btnPlays" onclick="setSortMode('plays')">Sort by Plays</button>
<button class="btn" id="btnRate" onclick="setSortMode('rate')">Sort by Rate %</button>
<button class="btn" onclick="toggleTheme()">Dark Mode 🌓</button>
</div>
<div class="table-container">
<table id="rateTable">
<thead>
<tr>
<th onclick="sortTable(0)">Song Title</th>
<th onclick="sortTable(1)">Normal</th>
<th onclick="sortTable(2)">Hard</th>
<th onclick="sortTable(3)">Expert</th>
<th onclick="sortTable(4)">Master</th>
</tr>
</thead>
<tbody>
{{TABLEBODY}}
</tbody>
</table>
</div>
<script>
let currentSortMode = 'plays';
let currentLang = localStorage.getItem('lang') || 'jp';
updateLanguageDisplay();
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const isDark = document.body.classList.contains('dark-mode');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
if (localStorage.getItem('theme') === 'dark') {
document.body.classList.add('dark-mode');
}
function toggleLanguage() {
currentLang = currentLang === 'jp' ? 'en' : 'jp';
localStorage.setItem('lang', currentLang);
updateLanguageDisplay();
}
function updateLanguageDisplay() {
const btn = document.getElementById('btnLang');
btn.innerText = 'Language: ' + (currentLang === 'jp' ? 'JP' : 'EN');
const titleCells = document.querySelectorAll('.title-cell');
titleCells.forEach(td => {
const newTitle = td.getAttribute(`data-title-${currentLang}`);
if (newTitle) {
td.innerText = newTitle;
td.setAttribute('data-val', newTitle);
}
});
}
function setSortMode(mode) {
currentSortMode = mode;
document.getElementById('btnPlays').classList.toggle('active', mode === 'plays');
document.getElementById('btnRate').classList.toggle('active', mode === 'rate');
}
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("rateTable");
switching = true;
dir = "desc";
var headers = table.getElementsByTagName("TH");
for (var h of headers) { h.classList.remove("asc", "desc"); }
while (switching) {
switching = false;
rows = table.rows;
for (i = 1; i < (rows.length - 1); i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
let attrName = (n === 0) ? 'data-val' : (currentSortMode === 'plays' ? 'data-plays' : 'data-rate');
var xVal = x.getAttribute(attrName);
var yVal = y.getAttribute(attrName);
var xNum = parseFloat(xVal);
var yNum = parseFloat(yVal);
if (!isNaN(xNum) && !isNaN(yNum)) {
if (dir == "asc") {
if (xNum > yNum) { shouldSwitch = true; break; }
} else if (dir == "desc") {
if (xNum < yNum) { shouldSwitch = true; break; }
}
} else {
// String sorting (for titles)
if (dir == "asc") {
if (xVal.toLowerCase() > yVal.toLowerCase()) { shouldSwitch = true; break; }
} else if (dir == "desc") {
if (xVal.toLowerCase() < yVal.toLowerCase()) { shouldSwitch = true; break; }
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount ++;
} else {
if (switchcount == 0 && dir == "desc") {
dir = "asc";
switching = true;
}
}
}
headers[n].classList.add(dir);
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,40 @@ lazy_static! {
} }
info info
}; };
pub static ref MUSIC: JsonValue = {
let mut info = object!{};
let items = json::parse(&include_file!("src/router/databases/json/music.json")).unwrap();
for live in LIVE_LIST.entries() {
info[live.1["id"].to_string()] = loop {
let mut val = object!{};
for data in items.members() {
if live.1["masterMusicId"] == data["id"] {
val = data.clone();
break;
}
}
break val;
};
};
info
};
pub static ref MUSIC_EN: JsonValue = {
let mut info = object!{};
let items = json::parse(&include_file!("src/router/databases/json/global/music.json")).unwrap();
for live in LIVE_LIST.entries() {
info[live.1["id"].to_string()] = loop {
let mut val = object!{};
for data in items.members() {
if live.1["masterMusicId"] == data["id"] {
val = data.clone();
break;
}
}
break val;
};
};
info
};
pub static ref RANKS: JsonValue = { pub static ref RANKS: JsonValue = {
json::parse(&include_file!("src/router/databases/json/user_rank.json")).unwrap() json::parse(&include_file!("src/router/databases/json/user_rank.json")).unwrap()
}; };

View File

@@ -9,40 +9,54 @@ use uuid::Uuid;
use crate::encryption; use crate::encryption;
use crate::router::{userdata, gree, items}; use crate::router::{userdata, gree, items};
use crate::runtime::get_easter_mode;
pub const ASSET_VERSION: &str = "5260ff15dff8ba0c00ad91400f515f55"; pub const ASSET_VERSION_GL: &str = "5260ff15dff8ba0c00ad91400f515f55";
pub const ASSET_HASH_ANDROID: &str = "d210b28037885f3ef56b8f8aa45ac95b"; pub const ASSET_HASH_ANDROID_GL: &str = "d210b28037885f3ef56b8f8aa45ac95b";
pub const ASSET_HASH_IOS: &str = "dd7175e4bcdab476f38c33c7f34b5e4d"; pub const ASSET_HASH_IOS_GL: &str = "dd7175e4bcdab476f38c33c7f34b5e4d";
pub const ASSET_VERSION_JP: &str = "4c921d2443335e574a82e04ec9ea243c"; pub const ASSET_VERSION_JP: &str = "4c921d2443335e574a82e04ec9ea243c";
pub const ASSET_HASH_ANDROID_JP: &str = "67f8f261c16b3cca63e520a25aad6c1c"; pub const ASSET_HASH_ANDROID_JP: &str = "67f8f261c16b3cca63e520a25aad6c1c";
pub const ASSET_HASH_IOS_JP: &str = "b8975be8300013a168d061d3fdcd4a16"; pub const ASSET_HASH_IOS_JP: &str = "b8975be8300013a168d061d3fdcd4a16";
pub const ASSET_HASH_ANDROID_EASTER_GL: &str = "da7ae831381c3f29337caa9891db7e6a";
pub const ASSET_HASH_ANDROID_EASTER_JP: &str = "eac0cad61c82bf2e31fc596555747d11";
pub fn get_asset_hash(asset_version: String, android: bool) -> String { pub fn get_asset_hash(asset_version: String, android: bool) -> String {
let args = crate::get_args(); let args = crate::get_args();
if asset_version == ASSET_VERSION_JP {
if android { if android {
if asset_version == ASSET_VERSION_JP {
if args.jp_android_asset_hash != String::new() { if args.jp_android_asset_hash != String::new() {
args.jp_android_asset_hash &args.jp_android_asset_hash
} else if get_easter_mode() {
ASSET_HASH_ANDROID_EASTER_JP
} else { } else {
ASSET_HASH_ANDROID_JP.to_string() ASSET_HASH_ANDROID_JP
} }
} else if args.jp_ios_asset_hash != String::new() {
args.jp_ios_asset_hash
} else { } else {
ASSET_HASH_IOS_JP.to_string()
}
} else if android {
if args.en_android_asset_hash != String::new() { if args.en_android_asset_hash != String::new() {
args.en_android_asset_hash &args.en_android_asset_hash
} else if get_easter_mode() {
ASSET_HASH_ANDROID_EASTER_GL
} else { } else {
ASSET_HASH_ANDROID.to_string() ASSET_HASH_ANDROID_GL
}
} }
} else if args.en_ios_asset_hash != String::new() {
args.en_ios_asset_hash
} else { } else {
ASSET_HASH_IOS.to_string() if asset_version == ASSET_VERSION_JP {
if args.jp_ios_asset_hash != String::new() {
&args.jp_ios_asset_hash
} else {
ASSET_HASH_IOS_JP
} }
} else {
if args.en_ios_asset_hash != String::new() {
&args.en_ios_asset_hash
} else {
ASSET_HASH_IOS_GL
}
}
}.to_string()
} }
pub fn create_token() -> String { pub fn create_token() -> String {

View File

@@ -15,7 +15,6 @@ use rsa::pkcs8::DecodePublicKey;
use crate::router::global; use crate::router::global;
use crate::router::userdata; use crate::router::userdata;
use crate::encryption; use crate::encryption;
use crate::router::user::{code_to_uid, uid_to_code};
use crate::sql::SQLite; use crate::sql::SQLite;
lazy_static! { lazy_static! {
@@ -231,11 +230,9 @@ pub fn migration_verify(req: HttpRequest, body: String) -> HttpResponse {
let body = json::parse(&body).unwrap(); let body = json::parse(&body).unwrap();
let password = decrypt_transfer_password(&body["migration_password"].to_string()); let password = decrypt_transfer_password(&body["migration_password"].to_string());
let uid = code_to_uid(body["migration_code"].to_string()).parse::<i64>().unwrap_or(0); let user = userdata::user::migration::get_acc_transfer(&body["migration_code"].to_string(), &password);
let user = userdata::get_acc_transfer(uid, &body["migration_code"].to_string(), &password); let resp = if !user["success"].as_bool().unwrap() || user["user_id"] == 0 {
let resp = if !user["success"].as_bool().unwrap() || uid == 0 {
object!{ object!{
result: "ERR", result: "ERR",
messsage: "User Not Found" messsage: "User Not Found"
@@ -245,7 +242,7 @@ pub fn migration_verify(req: HttpRequest, body: String) -> HttpResponse {
object!{ object!{
result: "OK", result: "OK",
src_uuid: user["login_token"].clone(), src_uuid: user["login_token"].clone(),
src_x_uid: uid.to_string(), src_x_uid: user["user_id"].to_string(),
migration_token: user["login_token"].clone(), migration_token: user["login_token"].clone(),
balance_charge_gem: data_user["gem"]["charge"].to_string(), balance_charge_gem: data_user["gem"]["charge"].to_string(),
balance_free_gem: data_user["gem"]["free"].to_string(), balance_free_gem: data_user["gem"]["free"].to_string(),
@@ -316,7 +313,7 @@ pub fn migration_code(req: HttpRequest) -> HttpResponse {
let resp = object!{ let resp = object!{
result: "OK", result: "OK",
migration_code: uid_to_code(user["user"]["id"].to_string()) migration_code: userdata::user::migration::get_acc_token(user["user"]["id"].as_i64().unwrap())
}; };
send(req, resp) send(req, resp)
@@ -336,10 +333,10 @@ pub fn migration_password_register(req: HttpRequest, body: String) -> HttpRespon
} }
let user = userdata::get_acc(&uid); let user = userdata::get_acc(&uid);
let code = uid_to_code(user["user"]["id"].to_string());
let pass = decrypt_transfer_password(&body["migration_password"].to_string()); let pass = decrypt_transfer_password(&body["migration_password"].to_string());
userdata::save_acc_transfer(&code, &pass); userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &pass);
let resp = object!{ let resp = object!{
result: "OK" result: "OK"

View File

@@ -182,6 +182,10 @@ fn check_for_stale_data(server_data: &mut JsonValue, live_id: i64) {
if live["expire_date_time"].as_u64().unwrap() < curr_time || live["master_live_id"] == live_id { if live["expire_date_time"].as_u64().unwrap() < curr_time || live["master_live_id"] == live_id {
expired.push(i).unwrap(); expired.push(i).unwrap();
} }
if live["expire_date_time"].as_u64().unwrap() < curr_time {
// User closed game after losing. Count this as a fail.
live_completed(live["master_live_id"].as_i64().unwrap(), live["level"].as_i32().unwrap(), true, 0, 0);
}
} }
for i in expired.members() { for i in expired.members() {
server_data["last_live_started"].array_remove(i.as_usize().unwrap()); server_data["last_live_started"].array_remove(i.as_usize().unwrap());
@@ -225,8 +229,8 @@ fn start_live(login_token: &str, body: &JsonValue) {
} }
check_for_stale_data(&mut server_data, body["master_live_id"].as_i64().unwrap()); check_for_stale_data(&mut server_data, body["master_live_id"].as_i64().unwrap());
let mut to_save = body.clone(); let mut to_save = body.clone();
// The user has 24 hours to complete a live // The user has 1 hour to complete a live
to_save["expire_date_time"] = (global::timestamp() + (24 * 60 * 60)).into(); to_save["expire_date_time"] = (global::timestamp() + (1 * 60 * 60)).into();
server_data["last_live_started"].push(to_save).unwrap(); server_data["last_live_started"].push(to_save).unwrap();
userdata::save_server_data(login_token, server_data); userdata::save_server_data(login_token, server_data);

View File

@@ -5,7 +5,7 @@ use crate::encryption;
use crate::router::{userdata, global}; use crate::router::{userdata, global};
fn get_asset_hash(req: &HttpRequest, body: &JsonValue) -> String { fn get_asset_hash(req: &HttpRequest, body: &JsonValue) -> String {
if body["asset_version"] != global::ASSET_VERSION && body["asset_version"] != global::ASSET_VERSION_JP { if body["asset_version"] != global::ASSET_VERSION_GL && body["asset_version"] != global::ASSET_VERSION_JP {
println!("Warning! Asset version is not what was expected. (Did the app update?)"); println!("Warning! Asset version is not what was expected. (Did the app update?)");
} }

View File

@@ -169,41 +169,10 @@ pub fn announcement(req: HttpRequest) -> Option<JsonValue> {
}) })
} }
pub fn uid_to_code(uid: String) -> String {
//just replace uid with numbers because im too lazy to have a real database and this is close enough anyways
uid
.replace('1', "A")
.replace('2', "G")
.replace('3', "W")
.replace('4', "Q")
.replace('5', "Y")
.replace('6', "6")
.replace('7', "I")
.replace('8', "P")
.replace('9', "U")
.replace('0', "M")
+ "7"
}
pub fn code_to_uid(code: String) -> String {
//just replace uid with numbers because im too lazy to have a real database and this is close enough anyways
code
.replace('7', "")
.replace('A', "1")
.replace('G', "2")
.replace('W', "3")
.replace('Q', "4")
.replace('Y', "5")
.replace('6', "6")
.replace('I', "7")
.replace('P', "8")
.replace('U', "9")
.replace('M', "0")
}
pub fn get_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> { pub fn get_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap(); let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let code = uid_to_code(body["user_id"].to_string()); let code = userdata::user::migration::get_acc_token(body["user_id"].as_i64()?);
Some(object!{ Some(object!{
"migrationCode": code "migrationCode": code
@@ -215,9 +184,8 @@ pub fn register_password(req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap(); let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let user = userdata::get_acc(&key); let user = userdata::get_acc(&key);
let code = uid_to_code(user["user"]["id"].to_string());
userdata::save_acc_transfer(&code, &body["pass"].to_string()); userdata::user::migration::save_acc_transfer(user["user"]["id"].as_i64().unwrap(), &body["pass"].to_string());
Some(array![]) Some(array![])
} }
@@ -225,18 +193,16 @@ pub fn register_password(req: HttpRequest, body: String) -> Option<JsonValue> {
pub fn verify_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> { pub fn verify_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap(); let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let uid = code_to_uid(body["migrationCode"].to_string()).parse::<i64>().unwrap_or(0); let user = userdata::user::migration::get_acc_transfer(&body["migrationCode"].to_string(), &body["pass"].to_string());
let user = userdata::get_acc_transfer(uid, &body["migrationCode"].to_string(), &body["pass"].to_string()); if !user["success"].as_bool().unwrap() || user["user_id"] == 0 {
if !user["success"].as_bool().unwrap() || uid == 0 {
return None; return None;
} }
let data_user = userdata::get_acc(&user["login_token"].to_string()); let data_user = userdata::get_acc(&user["login_token"].to_string());
Some(object!{ Some(object!{
"user_id": uid, "user_id": user["user_id"].clone(),
"uuid": user["login_token"].to_string(), "uuid": user["login_token"].to_string(),
"charge": data_user["gem"]["charge"].clone(), "charge": data_user["gem"]["charge"].clone(),
"free": data_user["gem"]["free"].clone() "free": data_user["gem"]["free"].clone()
@@ -245,11 +211,9 @@ pub fn verify_migration_code(_req: HttpRequest, body: String) -> Option<JsonValu
pub fn request_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> { pub fn request_migration_code(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap(); let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let uid = code_to_uid(body["migrationCode"].to_string()).parse::<i64>().unwrap_or(0); let user = userdata::user::migration::get_acc_transfer(&body["migrationCode"].to_string(), &body["pass"].to_string());
let user = userdata::get_acc_transfer(uid, &body["migrationCode"].to_string(), &body["pass"].to_string()); if !user["success"].as_bool().unwrap() || user["user_id"] == 0 {
if !user["success"].as_bool().unwrap() || uid == 0 {
return None; return None;
} }
@@ -440,7 +404,7 @@ pub fn initialize(req: HttpRequest, body: String) -> Option<JsonValue> {
for (i, data) in cardstoreward.members().enumerate() { for (i, data) in cardstoreward.members().enumerate() {
items::give_character(data.as_i64().unwrap(), &mut user, &mut missions, &mut array![], &mut array![]); items::give_character(data.as_i64().unwrap(), &mut user, &mut missions, &mut array![], &mut array![]);
if i < 10 { if i < 9 {
user["deck_list"][0]["main_card_ids"][i] = data.clone(); user["deck_list"][0]["main_card_ids"][i] = data.clone();
} }
} }

View File

@@ -1,9 +1,9 @@
pub mod user;
use rusqlite::params; use rusqlite::params;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use json::{JsonValue, array, object}; use json::{JsonValue, array, object};
use rand::Rng; use rand::Rng;
use sha2::{Digest, Sha256};
use base64::{Engine as _, engine::general_purpose};
use crate::router::global; use crate::router::global;
use crate::router::items; use crate::router::items;
@@ -17,16 +17,17 @@ lazy_static! {
}; };
} }
fn get_userdata_database() -> &'static SQLite {
&DATABASE
}
fn setup_tables(conn: &rusqlite::Connection) { fn setup_tables(conn: &rusqlite::Connection) {
user::migration::setup_sql(conn).unwrap();
conn.execute_batch(" conn.execute_batch("
CREATE TABLE IF NOT EXISTS tokens ( CREATE TABLE IF NOT EXISTS tokens (
user_id BIGINT NOT NULL PRIMARY KEY, user_id BIGINT NOT NULL PRIMARY KEY,
token TEXT NOT NULL token TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS migration (
token TEXT NOT NULL PRIMARY KEY,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS userdata ( CREATE TABLE IF NOT EXISTS userdata (
user_id BIGINT NOT NULL PRIMARY KEY, user_id BIGINT NOT NULL PRIMARY KEY,
userdata TEXT NOT NULL, userdata TEXT NOT NULL,
@@ -301,67 +302,6 @@ pub fn save_acc_sif(auth_key: &str, data: JsonValue) {
save_data(auth_key, "sifcards", data); save_data(auth_key, "sifcards", data);
} }
fn generate_salt() -> Vec<u8> {
let mut rng = rand::rng();
let mut bytes = vec![0u8; 16];
rng.fill(&mut bytes[..]);
bytes
}
fn hash_password(password: &str) -> String {
let salt = &generate_salt();
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let hashed_password = hasher.finalize();
let salt_hash = [&salt[..], &hashed_password[..]].concat();
general_purpose::STANDARD.encode(salt_hash)
}
fn verify_password(password: &str, salted_hash: &str) -> bool {
if password.is_empty() || salted_hash.is_empty() {
return false;
}
let bytes = general_purpose::STANDARD.decode(salted_hash);
if bytes.is_err() {
return password == salted_hash;
}
let bytes = bytes.unwrap();
if bytes.len() < 17 {
return password == salted_hash;
}
let (salt, hashed_password) = bytes.split_at(16);
let hashed_password = &hashed_password[0..32];
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let input_hash = hasher.finalize();
input_hash.as_slice() == hashed_password
}
pub fn get_acc_transfer(uid: i64, token: &str, password: &str) -> JsonValue {
let data = DATABASE.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(token));
if data.is_err() {
return object!{success: false};
}
if verify_password(password, &data.unwrap()) {
let login_token = get_login_token(uid);
if login_token == String::new() {
return object!{success: false};
}
return object!{success: true, login_token: login_token};
}
object!{success: false}
}
pub fn save_acc_transfer(token: &str, password: &str) {
DATABASE.lock_and_exec("DELETE FROM migration WHERE token=?1", params!(token));
DATABASE.lock_and_exec("INSERT INTO migration (token, password) VALUES (?1, ?2)", params!(token, hash_password(password)));
}
pub fn get_name_and_rank(uid: i64) -> JsonValue { pub fn get_name_and_rank(uid: i64) -> JsonValue {
let login_token = get_login_token(uid); let login_token = get_login_token(uid);
if login_token == String::new() { if login_token == String::new() {
@@ -479,8 +419,8 @@ fn create_webui_token() -> String {
} }
pub fn webui_login(uid: i64, password: &str) -> Result<String, String> { pub fn webui_login(uid: i64, password: &str) -> Result<String, String> {
let pass = DATABASE.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(crate::router::user::uid_to_code(uid.to_string()))).unwrap_or_default(); let pass = DATABASE.lock_and_select("SELECT password FROM migration WHERE user_id=?1", params!(uid)).unwrap_or_default();
if !verify_password(password, &pass) { if !user::migration::verify_password(password, &pass) {
if acc_exists(uid) && pass.is_empty() { if acc_exists(uid) && pass.is_empty() {
return Err(String::from("Migration token not set. Set token in game settings.")); return Err(String::from("Migration token not set. Set token in game settings."));
} }
@@ -505,13 +445,12 @@ pub fn webui_import_user(user: JsonValue) -> Result<JsonValue, String> {
let token = global::create_token(); let token = global::create_token();
DATABASE.lock_and_exec("INSERT INTO tokens (user_id, token) VALUES (?1, ?2)", params!(uid, token)); DATABASE.lock_and_exec("INSERT INTO tokens (user_id, token) VALUES (?1, ?2)", params!(uid, token));
let mig = crate::router::user::uid_to_code(uid.to_string());
save_acc_transfer(&mig, &user["password"].to_string()); let token = user::migration::save_acc_transfer(uid, &user["password"].to_string());
Ok(object!{ Ok(object!{
uid: uid, uid: uid,
migration_token: mig migration_token: token
}) })
} }
@@ -640,7 +579,7 @@ pub fn purge_accounts() -> usize {
DATABASE.lock_and_exec("DELETE FROM server_data WHERE user_id=?1", params!(user_id)); DATABASE.lock_and_exec("DELETE FROM server_data WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(user_id)); DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM tokens WHERE user_id=?1", params!(user_id)); DATABASE.lock_and_exec("DELETE FROM tokens WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM migration WHERE token=?1", params!(crate::router::user::uid_to_code(user_id.to_string()))); DATABASE.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(user_id));
} }
DATABASE.lock_and_exec("VACUUM", params!()); DATABASE.lock_and_exec("VACUUM", params!());
crate::router::gree::vacuum_database(); crate::router::gree::vacuum_database();

View File

@@ -0,0 +1 @@
pub mod migration;

View File

@@ -0,0 +1,157 @@
use rusqlite::params;
use json::{JsonValue, object};
use crate::router::userdata;
use rand::Rng;
use sha2::{Digest, Sha256};
use base64::{Engine as _, engine::general_purpose};
fn generate_token() -> String {
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::rng();
let random_string: String = (0..16)
.map(|_| {
let idx = rng.random_range(0..charset.len());
charset.chars().nth(idx).unwrap()
})
.collect();
random_string
}
pub fn get_acc_transfer(token: &str, password: &str) -> JsonValue {
let database = userdata::get_userdata_database();
let data = database.lock_and_select("SELECT password FROM migration WHERE token=?1", params!(token));
if data.is_err() {
return object!{success: false};
}
if verify_password(password, &data.unwrap()) {
let uid: i64 = database.lock_and_select_type("SELECT user_id FROM migration WHERE token=?1", params!(token)).unwrap();
let login_token = userdata::get_login_token(uid);
if login_token == String::new() {
return object!{success: false};
}
return object!{success: true, login_token: login_token, user_id: uid};
}
object!{success: false}
}
pub fn save_acc_transfer(uid: i64, password: &str) -> String {
let database = userdata::get_userdata_database();
let token = if let Ok(value) = database.lock_and_select("SELECT token FROM migration WHERE user_id=?1", params!(uid)) {
value
} else {
generate_token()
};
database.lock_and_exec("DELETE FROM migration WHERE user_id=?1", params!(uid));
database.lock_and_exec("INSERT INTO migration (user_id, token, password) VALUES (?1, ?2, ?3)", params!(uid, &token, hash_password(password)));
token
}
pub fn get_acc_token(uid: i64) -> String {
let database = userdata::get_userdata_database();
if let Ok(value) = database.lock_and_select("SELECT token FROM migration WHERE user_id=?1", params!(uid)) {
value
} else {
save_acc_transfer(uid, "")
}
}
fn generate_salt() -> Vec<u8> {
let mut rng = rand::rng();
let mut bytes = vec![0u8; 16];
rng.fill(&mut bytes[..]);
bytes
}
fn hash_password(password: &str) -> String {
if password.is_empty() { return String::new(); };
let salt = &generate_salt();
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let hashed_password = hasher.finalize();
let salt_hash = [&salt[..], &hashed_password[..]].concat();
general_purpose::STANDARD.encode(salt_hash)
}
pub fn verify_password(password: &str, salted_hash: &str) -> bool {
if password.is_empty() || salted_hash.is_empty() {
return false;
}
let bytes = general_purpose::STANDARD.decode(salted_hash);
if bytes.is_err() {
return password == salted_hash;
}
let bytes = bytes.unwrap();
if bytes.len() < 17 {
return password == salted_hash;
}
let (salt, hashed_password) = bytes.split_at(16);
let hashed_password = &hashed_password[0..32];
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let input_hash = hasher.finalize();
input_hash.as_slice() == hashed_password
}
pub fn setup_sql(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
conn.execute("
CREATE TABLE IF NOT EXISTS migration (
user_id BIGINT NOT NULL,
token TEXT NOT NULL,
password TEXT NOT NULL,
PRIMARY KEY (user_id, token)
);
", [])?;
let is_updated = conn.prepare("SELECT user_id FROM migration LIMIT 1;").is_ok();
if is_updated { return Ok(()); }
println!("Upgrading migration table");
conn.execute("DROP TABLE IF EXISTS migration_new;", [])?;
conn.execute("
CREATE TABLE migration_new (
user_id BIGINT NOT NULL,
token TEXT NOT NULL,
password TEXT NOT NULL,
PRIMARY KEY (user_id, token)
);
", [])?;
let mut stmt = conn.prepare("SELECT token, password FROM migration")?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
))
})?;
let mut insert_new_row = conn.prepare("INSERT INTO migration_new (user_id, token, password) VALUES (?1, ?2, ?3)")?;
for row in rows {
let (token, password) = row?;
let user_id = code_to_uid(&token);
insert_new_row.execute(params![user_id, token, password])?;
}
conn.execute("DROP TABLE migration;", params!())?;
conn.execute("ALTER TABLE migration_new RENAME TO migration;", params!())?;
Ok(())
}
fn code_to_uid(code: &str) -> String {
code
.replace('7', "")
.replace('A', "1")
.replace('G', "2")
.replace('W', "3")
.replace('Q', "4")
.replace('Y', "5")
.replace('6', "6")
.replace('I', "7")
.replace('P', "8")
.replace('U', "9")
.replace('M', "0")
}

View File

@@ -1,17 +1,44 @@
use crate::lock_onto_mutex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::Mutex; use std::sync::RwLock;
use std::fs;
lazy_static! { lazy_static! {
static ref RUNNING: Mutex<bool> = Mutex::new(false); static ref RUNNING: RwLock<bool> = RwLock::new(false);
static ref DATAPATH: RwLock<String> = RwLock::new(String::new());
static ref EASTER: RwLock<bool> = RwLock::new(false);
} }
pub fn set_running(running: bool) { pub fn set_running(running: bool) {
let mut result = lock_onto_mutex!(RUNNING); let mut w = RUNNING.write().unwrap();
*result = running; *w = running;
} }
pub fn get_running() -> bool { pub fn get_running() -> bool {
let result = lock_onto_mutex!(RUNNING); *RUNNING.read().unwrap()
*result }
pub fn get_data_path(file_name: &str) -> String {
let mut path = {
DATAPATH.read().unwrap().clone()
};
while path.ends_with('/') {
path.pop();
}
fs::create_dir_all(&path).unwrap();
format!("{}/{}", path, file_name)
}
pub fn update_data_path(path: &str) {
let mut w = DATAPATH.write().unwrap();
*w = path.to_string();
}
// Only currently editable by the android so
pub fn set_easter_mode(enabled: bool) {
let mut w = EASTER.write().unwrap();
*w = enabled;
}
pub fn get_easter_mode() -> bool {
*EASTER.read().unwrap()
} }

View File

@@ -33,6 +33,13 @@ impl SQLite {
} }
}) })
} }
pub fn lock_and_select_type<T: rusqlite::types::FromSql>(&self, command: &str, args: &[&dyn ToSql]) -> Result<T, rusqlite::Error> {
let conn = Connection::open(&self.path).unwrap();
let mut stmt = conn.prepare(command)?;
stmt.query_row(args, |row| {
row.get(0)
})
}
pub fn lock_and_select_all(&self, command: &str, args: &[&dyn ToSql]) -> Result<JsonValue, rusqlite::Error> { pub fn lock_and_select_all(&self, command: &str, args: &[&dyn ToSql]) -> Result<JsonValue, rusqlite::Error> {
let conn = Connection::open(&self.path).unwrap(); let conn = Connection::open(&self.path).unwrap();
let mut stmt = conn.prepare(command)?; let mut stmt = conn.prepare(command)?;

View File

@@ -26,7 +26,29 @@ async fn maintenance(_req: HttpRequest) -> HttpResponse {
.body(r#"{"opened_at":"2024-02-05 02:00:00","closed_at":"2024-02-05 04:00:00","message":":(","server":1,"gamelib":0}"#) .body(r#"{"opened_at":"2024-02-05 02:00:00","closed_at":"2024-02-05 04:00:00","message":":(","server":1,"gamelib":0}"#)
} }
#[cfg(feature = "library")]
use include_dir::{include_dir, Dir};
#[cfg(all(feature = "library", target_os = "ios"))]
static SPART_FILES: Dir<'_> = include_dir!("assets/iOS/");
#[cfg(all(feature = "library", target_os = "android"))]
static SPART_FILES: Dir<'_> = include_dir!("assets/Android/");
fn handle_assets(req: HttpRequest) -> HttpResponse { fn handle_assets(req: HttpRequest) -> HttpResponse {
#[cfg(feature = "library")]
{
let lang: String = req.match_info().get("lang").unwrap_or("JP").parse().unwrap_or(String::from("JP"));
let file_name: String = req.match_info().get("file").unwrap().parse().unwrap();
let hash: String = req.match_info().get("file").unwrap().parse().unwrap();
if let Some(file) = SPART_FILES.get_file(format!("{lang}/{hash}/{file_name}")) {
let body = file.contents();
return HttpResponse::Ok()
.insert_header(ContentType(mime::APPLICATION_OCTET_STREAM))
.insert_header(("content-length", body.len()))
.body(body);
}
}
let file_path = format!("assets{}", req.path()); let file_path = format!("assets{}", req.path());
let exists = fs::exists(&file_path); let exists = fs::exists(&file_path);