CI: only the mod on push (1 job, low priority); the game on demand, refused when the disk is low
build / mod (push) Successful in 5m9s
build / mod (push) Successful in 5m9s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+15
-44
@@ -1,6 +1,7 @@
|
||||
# Сборка на каждый пуш: мод (.hmod, для всех ОС) и игра-форк для Linux, в одной задаче
|
||||
# (без передачи артефактов между задачами). Пуш в main публикует релиз build-<номер>.
|
||||
# Сборки для Windows и macOS — desktop.yml (вручную).
|
||||
# На каждый пуш: только мод (.hmod, один для всех ОС) и релиз build-<номер> с ним.
|
||||
# Раннер живёт на одной машине с Gitea и другими сервисами (2 ядра, 4 ГБ): сборка мода лёгкая —
|
||||
# один поток компиляции, низший приоритет, без системных пакетов и без кэша target.
|
||||
# Игра-форк собирается отдельно и только вручную: game.yml (Linux) и desktop.yml (macOS/Windows).
|
||||
name: build
|
||||
|
||||
on:
|
||||
@@ -9,55 +10,28 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: never
|
||||
# The runner shares its machine with Gitea: two compile jobs at a time, no incremental
|
||||
# files, so the build doesn't starve the server (Gitea answered 500 under a full build).
|
||||
CARGO_BUILD_JOBS: "2"
|
||||
CARGO_BUILD_JOBS: "1"
|
||||
CARGO_INCREMENTAL: "0"
|
||||
# Lowest CPU priority for the long steps (ionice too where the image has it, below).
|
||||
LOW: nice -n 19
|
||||
|
||||
jobs:
|
||||
build:
|
||||
mod:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Системные библиотеки
|
||||
run: |
|
||||
SUDO=$(command -v sudo || true)
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y -qq build-essential pkg-config libasound2-dev libudev-dev
|
||||
- name: Низкий приоритет
|
||||
run: command -v ionice >/dev/null && echo "LOW=nice -n 19 ionice -c 3" >> "$GITHUB_ENV" || true
|
||||
- name: Rust
|
||||
run: |
|
||||
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
# Only the small things are cached: the build's target folder is gigabytes, and moving it
|
||||
# through the cache server timed out and loaded the machine.
|
||||
# Инструмент упаковки (cargo-horizon-mod без пробного запуска) — единственное, что стоит кэшировать.
|
||||
- uses: actions/cache@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: cargo-${{ hashFiles('HORIZON_COMMIT') }}
|
||||
restore-keys: cargo-
|
||||
path: ~/.cargo/bin/cargo-horizon-mod
|
||||
key: tool-${{ hashFiles('HORIZON_COMMIT', 'horizon-patches/*') }}
|
||||
- name: Форк Horizon
|
||||
run: $LOW python3 tools/airdef.py fork
|
||||
run: nice -n 19 python3 tools/airdef.py fork
|
||||
- name: Мод
|
||||
run: $LOW python3 tools/airdef.py mod
|
||||
- name: Игра
|
||||
run: $LOW python3 tools/airdef.py game
|
||||
- name: Архивы
|
||||
run: |
|
||||
mkdir -p out
|
||||
cp airdef-mod/target/horizon-mod/airdef.hmod out/airdef.hmod
|
||||
D=airdef-linux-x86_64
|
||||
mkdir -p "$D"
|
||||
cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl out/airdef.hmod dist/play.sh docs/INSTALL.md "$D/"
|
||||
tar czf "out/$D.tar.gz" "$D"
|
||||
ls -la out
|
||||
run: nice -n 19 python3 tools/airdef.py mod
|
||||
- name: Релиз
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
@@ -67,17 +41,14 @@ jobs:
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -e
|
||||
BODY=$(printf 'Сборка %s (%s).\n\n- airdef.hmod — мод, для всех ОС;\n- airdef-linux-x86_64.tar.gz — игра-форк Horizon для Linux с модом и play.sh.\n\nСборки для Windows и macOS добавляет workflow desktop (запуск вручную с этим тегом).\nКак запускать: docs/INSTALL.md.' "$TAG" "$SHA")
|
||||
mkdir -p out && cp airdef-mod/target/horizon-mod/airdef.hmod out/airdef.hmod
|
||||
BODY=$(printf 'Сборка %s (%s).\n\n- airdef.hmod — мод, для всех ОС.\n\nИгра-форк Horizon (нужна для мода): собрать у себя — python3 tools/airdef.py all, или запустить вручную workflow game (Linux) / desktop (macOS, Windows) с этим тегом.\nКак запускать: docs/INSTALL.md.' "$TAG" "$SHA")
|
||||
DATA=$(BODY="$BODY" python3 -c 'import json,os;print(json.dumps({"tag_name":os.environ["TAG"],"target_commitish":os.environ["SHA"],"name":os.environ["TAG"],"body":os.environ["BODY"]}))')
|
||||
if [ -z "$TOKEN" ]; then echo "::error::нет токена: добавь секрет RELEASE_TOKEN"; exit 1; fi
|
||||
CODE=$(curl -sS -o release.json -w '%{http_code}' -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "$API/releases" -d "$DATA")
|
||||
if [ "$CODE" != "201" ]; then
|
||||
echo "::error::Gitea не создала релиз (HTTP $CODE): $(cat release.json). Встроенному токену может не хватать прав — добавь секрет RELEASE_TOKEN (токен с правом записи в репозиторий)."
|
||||
echo "::error::Gitea не создала релиз (HTTP $CODE): $(cat release.json). Проверь секрет RELEASE_TOKEN (токен с правом записи в репозиторий)."
|
||||
exit 1
|
||||
fi
|
||||
ID=$(python3 -c 'import json;print(json.load(open("release.json"))["id"])')
|
||||
for f in out/*; do
|
||||
name=$(basename "$f")
|
||||
echo "upload $name"
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@$f" "$API/releases/$ID/assets?name=$name" > /dev/null
|
||||
done
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef.hmod" "$API/releases/$ID/assets?name=airdef.hmod" > /dev/null
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Игра-форк для Linux: только вручную (Actions → game → Run workflow) с тегом готового релиза
|
||||
# (build-N), дописывает в него airdef-linux-x86_64.tar.gz. Тяжёлая сборка (час на 2 ядрах,
|
||||
# несколько ГБ диска и памяти), поэтому один поток, низший приоритет, и отказ, если на сервере
|
||||
# мало места: забитый диск роняет Gitea.
|
||||
name: game
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Тег релиза, например build-7
|
||||
required: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: never
|
||||
CARGO_BUILD_JOBS: "1"
|
||||
CARGO_INCREMENTAL: "0"
|
||||
|
||||
jobs:
|
||||
game-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Место на диске
|
||||
run: |
|
||||
FREE=$(df --output=avail -BG / | tail -1 | tr -dc 0-9)
|
||||
echo "свободно: ${FREE} ГБ"
|
||||
if [ "$FREE" -lt 15 ]; then echo "::error::на сервере свободно ${FREE} ГБ, нужно хотя бы 15: сборка игры забьёт диск и уронит Gitea"; exit 1; fi
|
||||
- uses: actions/checkout@v4
|
||||
- name: Системные библиотеки
|
||||
run: |
|
||||
SUDO=$(command -v sudo || true)
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y -qq --no-install-recommends build-essential pkg-config libasound2-dev libudev-dev
|
||||
- name: Rust
|
||||
run: |
|
||||
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Форк Horizon
|
||||
run: nice -n 19 python3 tools/airdef.py fork
|
||||
- name: Мод
|
||||
run: nice -n 19 python3 tools/airdef.py mod
|
||||
- name: Игра
|
||||
run: nice -n 19 python3 tools/airdef.py game
|
||||
- name: В релиз
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
D=airdef-linux-x86_64
|
||||
mkdir -p "$D" out
|
||||
cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl airdef-mod/target/horizon-mod/airdef.hmod dist/play.sh docs/INSTALL.md "$D/"
|
||||
tar czf "out/$D.tar.gz" "$D"
|
||||
ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$ID/assets?name=$D.tar.gz" > /dev/null
|
||||
+7
-6
@@ -19,13 +19,14 @@ AirDef работает на своей сборке (форке) Horizon: в н
|
||||
|
||||
## 2а. Готовая сборка из релиза (проще)
|
||||
|
||||
В репозитории → **Releases** → последний `build-N`. Там лежат:
|
||||
В репозитории → **Releases** → последний `build-N`. В каждом релизе есть `airdef.hmod` — сам мод (один для всех
|
||||
ОС). Игру-форк сервер сборок не собирает сам: это тяжело для него. Архивы с игрой появляются в релизе, только если
|
||||
их собрали вручную:
|
||||
- `airdef-linux-x86_64.tar.gz` — Actions → **game** → Run workflow, указать тег релиза;
|
||||
- `airdef-windows-x86_64.zip`, `airdef-macos-arm64.tar.gz` — Actions → **desktop** (нужны раннеры с метками
|
||||
`windows-latest` / `macos-latest`).
|
||||
|
||||
- `airdef.hmod` — сам мод;
|
||||
- `airdef-windows-x86_64.zip`, `airdef-macos-arm64.tar.gz`, `airdef-linux-x86_64.tar.gz` — игра-форк + мод +
|
||||
скрипт запуска. Сборка для Linux есть всегда; для Windows и macOS её добавляет workflow **desktop**
|
||||
(Actions → desktop → Run workflow, указать тег релиза), если подключены раннеры с метками
|
||||
`windows-latest` / `macos-latest`. Нет такой сборки — способ 2б.
|
||||
Нет архива для твоей ОС — способ 2б: игра соберётся у тебя, а мод можно взять из релиза или собрать там же.
|
||||
|
||||
**Windows:** распакуй zip куда угодно и запусти `play.bat`. Он положит мод в папку игры, включит его и запустит
|
||||
игру. Конкретный мир: `play.bat --world "Имя мира"` (в командной строке, из папки архива).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:37:45 +0300
|
||||
Subject: [PATCH 1/9] Mod API (AirDef fork): world models, client particles,
|
||||
Subject: [PATCH 01/10] Mod API (AirDef fork): world models, client particles,
|
||||
sounds, ground heights
|
||||
|
||||
render.set-models draws block models with an interpolated pose per id;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:39:45 +0300
|
||||
Subject: [PATCH 2/9] cargo-horizon-mod: report particles and sounds
|
||||
Subject: [PATCH 02/10] cargo-horizon-mod: report particles and sounds
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 13:33:10 +0300
|
||||
Subject: [PATCH 3/9] Mod API (AirDef fork): full-screen post pass for mods
|
||||
Subject: [PATCH 03/10] Mod API (AirDef fork): full-screen post pass for mods
|
||||
|
||||
render.set-post compiles a mod's WGSL after a prelude (camera matrices,
|
||||
time, 256 vec4 of params, the world's colour and depth) and runs it after
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:09:30 +0300
|
||||
Subject: [PATCH 4/9] Mod models: draw far models as scaled impostors past the
|
||||
far plane, cull at 4096
|
||||
Subject: [PATCH 04/10] Mod models: draw far models as scaled impostors past
|
||||
the far plane, cull at 4096
|
||||
|
||||
---
|
||||
crates/horizon-client/src/game/mod_models.rs | 20 ++++++++++++++++----
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:31:00 +0300
|
||||
Subject: [PATCH 5/9] Mod cameras: render.set-views draws the world from other
|
||||
points into pictures that HUD view shapes show (API level 9)
|
||||
Subject: [PATCH 05/10] Mod cameras: render.set-views draws the world from
|
||||
other points into pictures that HUD view shapes show (API level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:08:21 +0300
|
||||
Subject: [PATCH 6/9] Mods: more fuel per tick and per event (AirDef plans
|
||||
Subject: [PATCH 06/10] Mods: more fuel per tick and per event (AirDef plans
|
||||
raids)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:25:29 +0300
|
||||
Subject: [PATCH 7/9] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
Subject: [PATCH 07/10] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:15:11 +0300
|
||||
Subject: [PATCH 8/9] Mod cameras: distant terrain tiles, fog measured from the
|
||||
camera, no impostors while one is shown
|
||||
Subject: [PATCH 08/10] Mod cameras: distant terrain tiles, fog measured from
|
||||
the camera, no impostors while one is shown
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:25:45 +0300
|
||||
Subject: [PATCH 9/9] Mods: game.world-id, so mods keep state per world (API
|
||||
Subject: [PATCH 09/10] Mods: game.world-id, so mods keep state per world (API
|
||||
level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 12:01:08 +0300
|
||||
Subject: [PATCH 10/10] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
packing-only tool skips wasmtime
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/cargo-horizon-mod/Cargo.toml | 8 +++++++-
|
||||
crates/cargo-horizon-mod/src/main.rs | 16 ++++++++++++++--
|
||||
2 files changed, 21 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/crates/cargo-horizon-mod/Cargo.toml b/crates/cargo-horizon-mod/Cargo.toml
|
||||
index 34eefe227..21b8a9221 100644
|
||||
--- a/crates/cargo-horizon-mod/Cargo.toml
|
||||
+++ b/crates/cargo-horizon-mod/Cargo.toml
|
||||
@@ -6,8 +6,14 @@ license.workspace = true
|
||||
rust-version = "1.86"
|
||||
description = "cargo horizon-mod new/build/run/check: make Horizon mods in Rust (docs/mods.md)"
|
||||
|
||||
+[features]
|
||||
+default = ["full"]
|
||||
+# The trial runs (`check`, `test`) and the registry's CI need wasmtime. Without this feature the
|
||||
+# tool is new, build and run only, and builds in a fraction of the time (CI that only packs mods).
|
||||
+full = ["horizon-mods/runtime", "horizon-mods/index"]
|
||||
+
|
||||
[dependencies]
|
||||
-horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack", "runtime", "index"] }
|
||||
+horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack"] }
|
||||
serde_json = "1.0.151"
|
||||
|
||||
[lints]
|
||||
diff --git a/crates/cargo-horizon-mod/src/main.rs b/crates/cargo-horizon-mod/src/main.rs
|
||||
index 95d0a9eb3..ff4ab663b 100644
|
||||
--- a/crates/cargo-horizon-mod/src/main.rs
|
||||
+++ b/crates/cargo-horizon-mod/src/main.rs
|
||||
@@ -11,12 +11,15 @@
|
||||
//! - `check <file> [--json]`: what the registry checks of a submission
|
||||
//! (`.hmod`, `.hpack`, resource pack `.zip`), with a trial run for a mod.
|
||||
//! - `registry build|review|check|list`: the registry repository's CI.
|
||||
+#[cfg(feature = "full")]
|
||||
+use horizon_mods::testing::{Script, TestGame};
|
||||
use horizon_mods::{
|
||||
Capability, Content, Manifest, Package,
|
||||
manifest::{MANIFEST, component_imports, componentize},
|
||||
- testing::{Script, TestGame},
|
||||
};
|
||||
+#[cfg(feature = "full")]
|
||||
mod check;
|
||||
+#[cfg(feature = "full")]
|
||||
mod registry;
|
||||
|
||||
use std::{
|
||||
@@ -39,9 +42,16 @@ fn main() -> ExitCode {
|
||||
println!("Built {}", p.display());
|
||||
}),
|
||||
Some("run") => run(&args[1..]),
|
||||
+ #[cfg(feature = "full")]
|
||||
Some("test") => test(&args[1..]),
|
||||
+ #[cfg(feature = "full")]
|
||||
Some("check") => check(&args[1..]),
|
||||
+ #[cfg(feature = "full")]
|
||||
Some("registry") => registry::main(&args[1..]),
|
||||
+ #[cfg(not(feature = "full"))]
|
||||
+ Some("test" | "check" | "registry") => {
|
||||
+ Err("this cargo-horizon-mod was built without trial runs (feature `full`)".into())
|
||||
+ }
|
||||
_ => Err("usage: cargo horizon-mod new <path> [--template code|content] | build [--debug] | run [--game <horizon-client>] [-- <game args>] | test [--debug] [--hmod <file>] [<name>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
|
||||
};
|
||||
match result {
|
||||
@@ -249,6 +259,7 @@ fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
|
||||
|
||||
/// `check <file>`: the registry's checks of a submission, printed; fails
|
||||
/// when one does.
|
||||
+#[cfg(feature = "full")]
|
||||
fn check(args: &[String]) -> Result<(), String> {
|
||||
let file = args
|
||||
.first()
|
||||
@@ -280,6 +291,7 @@ fn check(args: &[String]) -> Result<(), String> {
|
||||
|
||||
/// `test`: every `tests/*.toml` script (those whose name holds `<name>`)
|
||||
/// against the built mod, or the `.hmod` given with `--hmod`.
|
||||
+#[cfg(feature = "full")]
|
||||
fn test(args: &[String]) -> Result<(), String> {
|
||||
let hmod = match flag(args, "--hmod") {
|
||||
Some(p) => PathBuf::from(p),
|
||||
@@ -360,7 +372,7 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
.ok_or_else(|| format!("the game exited with {status}"))
|
||||
}
|
||||
|
||||
-#[cfg(test)]
|
||||
+#[cfg(all(test, feature = "full"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
+2
-1
@@ -88,7 +88,8 @@ def mod():
|
||||
if not Path(tool).exists():
|
||||
if not (FORK / "Cargo.toml").exists():
|
||||
fork()
|
||||
run([cargo(), "install", "--locked", "--path", FORK / "crates" / "cargo-horizon-mod"])
|
||||
# Without trial runs (wasmtime): building a mod needs only packing, a fraction of the compile.
|
||||
run([cargo(), "install", "--locked", "--no-default-features", "--path", FORK / "crates" / "cargo-horizon-mod"])
|
||||
run([cargo(), "horizon-mod", "build"], cwd=ROOT / "airdef-mod")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user