Add DEV workshop deployment workflow
Deploy DEV Workshop / deploy-dev-workshop (push) Failing after 1h15m47s
Deploy DEV Workshop / deploy-dev-workshop (push) Failing after 1h15m47s
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
name: Deploy DEV Workshop
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
STEAM_APP_ID: "107410"
|
||||||
|
BRAF_CI_CACHE_DIR: .ci-cache/dev-workshop
|
||||||
|
BRAF_CI_STAGE_DIR: dist/dev/@braf_dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-dev-workshop:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Restore PBO cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: .ci-cache/dev-workshop
|
||||||
|
key: braf-dev-workshop-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
braf-dev-workshop-
|
||||||
|
|
||||||
|
- name: Install Mikero tools
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libogg0 libuchardet0 libvorbis0a libvorbisenc2 libvorbisfile3
|
||||||
|
|
||||||
|
curl -fsSL \
|
||||||
|
https://github.com/arma-actions/mikero-tools/archive/refs/heads/master.tar.gz \
|
||||||
|
-o /tmp/mikero-tools-action.tar.gz
|
||||||
|
|
||||||
|
rm -rf /tmp/mikero-tools-action /opt/mikero-tools
|
||||||
|
mkdir -p /tmp/mikero-tools-action /opt/mikero-tools
|
||||||
|
tar -zxf /tmp/mikero-tools-action.tar.gz \
|
||||||
|
--strip-components=1 \
|
||||||
|
-C /tmp/mikero-tools-action
|
||||||
|
|
||||||
|
tar -xf /tmp/mikero-tools-action/linux/*.tar \
|
||||||
|
--strip-components=1 \
|
||||||
|
-C /opt/mikero-tools
|
||||||
|
|
||||||
|
echo "/opt/mikero-tools/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}:/opt/mikero-tools/lib" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect global build changes
|
||||||
|
id: changes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
before="${{ github.event.before }}"
|
||||||
|
after="${{ github.sha }}"
|
||||||
|
|
||||||
|
if [[ -z "${before}" || "${before}" == "0000000000000000000000000000000000000000" ]]; then
|
||||||
|
echo "force_rebuild=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
changed="$(git diff --name-only "${before}" "${after}" || printf '%s\n' '__force_rebuild__')"
|
||||||
|
printf '%s\n' "${changed}"
|
||||||
|
|
||||||
|
if printf '%s\n' "${changed}" | grep -Eq '^(__force_rebuild__|\.gitea/workflows/|make/|mod\.cpp$|meta\.cpp$)'; then
|
||||||
|
echo "force_rebuild=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "force_rebuild=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and stage DEV package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p "$BRAF_CI_CACHE_DIR"
|
||||||
|
|
||||||
|
args=(
|
||||||
|
--cache-dir "$BRAF_CI_CACHE_DIR"
|
||||||
|
--stage-dir "$BRAF_CI_STAGE_DIR"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ "${{ steps.changes.outputs.force_rebuild }}" == "true" ]]; then
|
||||||
|
args+=(--force-rebuild)
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 -m make.ci_dev_workshop "${args[@]}"
|
||||||
|
|
||||||
|
- name: Prepare changelog
|
||||||
|
id: changelog
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
short_sha="$(git rev-parse --short HEAD)"
|
||||||
|
subject="$(git log -1 --pretty=%s)"
|
||||||
|
echo "text=DEV ${short_sha}: ${subject}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Upload DEV Workshop item
|
||||||
|
uses: https://github.com/arma-actions/workshop-upload@v1
|
||||||
|
env:
|
||||||
|
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
|
||||||
|
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
|
||||||
|
with:
|
||||||
|
appId: ${{ env.STEAM_APP_ID }}
|
||||||
|
itemId: ${{ secrets.STEAM_DEV_ITEM_ID }}
|
||||||
|
contentPath: ${{ env.BRAF_CI_STAGE_DIR }}
|
||||||
|
changelog: ${{ steps.changelog.outputs.text }}
|
||||||
+3
-3
@@ -5,6 +5,6 @@ braf_factions/
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
env*
|
env*
|
||||||
braf_sar/.vscode/c_cpp_properties.json
|
*.vscode*
|
||||||
braf_sar/.vscode/launch.json
|
dist/
|
||||||
braf_sar/.vscode/settings.json
|
.ci-cache/
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build and stage the BRAF DEV Steam Workshop package.
|
||||||
|
|
||||||
|
The script keeps a persistent addon cache outside the checkout. Each run hashes
|
||||||
|
root-level braf_* addon folders, rebuilds only changed or missing PBOs with
|
||||||
|
Mikero pboProject, then stages a complete Arma 3 mod folder for upload.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from make.obsfuschatedList import obfuschatedList
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
from make.obsfuschatedList import obfuschatedList
|
||||||
|
|
||||||
|
|
||||||
|
MANIFEST_VERSION = 1
|
||||||
|
DEFAULT_STAGE_DIR = Path("dist/dev/@braf_dev")
|
||||||
|
METADATA_FILES = ("mod.cpp", "meta.cpp")
|
||||||
|
MOD_ASSET_FIELDS = ("picture", "logo", "logoOver", "logoSmall")
|
||||||
|
IGNORED_DIRS = {".git", "__pycache__"}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Build changed BRAF addons and stage the DEV workshop folder."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cache-dir",
|
||||||
|
default=os.getenv("BRAF_CI_CACHE_DIR"),
|
||||||
|
help="Persistent cache directory. Defaults to BRAF_CI_CACHE_DIR.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stage-dir",
|
||||||
|
default=os.getenv("BRAF_CI_STAGE_DIR", str(DEFAULT_STAGE_DIR)),
|
||||||
|
help=f"Output mod folder. Defaults to {DEFAULT_STAGE_DIR}.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--source-root",
|
||||||
|
default=os.getenv("BRAF_MIKERO_SOURCE_ROOT"),
|
||||||
|
help="Root passed to pboProject. Defaults to the repository checkout.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force-rebuild",
|
||||||
|
action="store_true",
|
||||||
|
help="Rebuild every buildable addon, ignoring matching cache hashes.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Print planned work without running pboProject or writing cache/stage output.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def buildable_addons(root: Path) -> list[str]:
|
||||||
|
addons: list[str] = []
|
||||||
|
for path in sorted(root.glob("braf_*")):
|
||||||
|
if path.is_dir() and (path / "config.cpp").is_file():
|
||||||
|
addons.append(path.name)
|
||||||
|
return addons
|
||||||
|
|
||||||
|
|
||||||
|
def file_digest(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def addon_digest(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for file_path in sorted(p for p in path.rglob("*") if p.is_file()):
|
||||||
|
if any(part in IGNORED_DIRS for part in file_path.relative_to(path).parts):
|
||||||
|
continue
|
||||||
|
rel = file_path.relative_to(path).as_posix()
|
||||||
|
digest.update(rel.encode("utf-8"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
digest.update(file_digest(file_path).encode("ascii"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def load_manifest(path: Path) -> dict[str, Any]:
|
||||||
|
if not path.is_file():
|
||||||
|
return {"version": MANIFEST_VERSION, "addons": {}}
|
||||||
|
|
||||||
|
with path.open("r", encoding="utf-8") as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
|
||||||
|
if data.get("version") != MANIFEST_VERSION or not isinstance(data.get("addons"), dict):
|
||||||
|
return {"version": MANIFEST_VERSION, "addons": {}}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def write_manifest(path: Path, manifest: dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = path.with_suffix(".tmp")
|
||||||
|
with tmp.open("w", encoding="utf-8") as handle:
|
||||||
|
json.dump(manifest, handle, indent=2, sort_keys=True)
|
||||||
|
handle.write("\n")
|
||||||
|
tmp.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def cached_pbo_exists(cache_addons_dir: Path, addon: str) -> bool:
|
||||||
|
return (cache_addons_dir / f"{addon}.pbo").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def addon_cache_files(cache_addons_dir: Path, addon: str) -> list[Path]:
|
||||||
|
patterns = (f"{addon}.pbo", f"{addon}.pbo.*.bisign")
|
||||||
|
files: list[Path] = []
|
||||||
|
for pattern in patterns:
|
||||||
|
files.extend(sorted(cache_addons_dir.glob(pattern)))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def remove_cached_addon(cache_addons_dir: Path, addon: str) -> None:
|
||||||
|
for path in addon_cache_files(cache_addons_dir, addon):
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_mikero_source_root(root: Path, cache_dir: Path, addons: list[str]) -> Path:
|
||||||
|
"""Create a P:/braf-like source root using symlinks.
|
||||||
|
|
||||||
|
The historic local build path was P:/braf/<addon>. Keeping the parent folder
|
||||||
|
named "braf" avoids changing Mikero's inferred addon prefix in CI.
|
||||||
|
"""
|
||||||
|
source_root = cache_dir / "mikero-source" / "braf"
|
||||||
|
source_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
wanted = set(addons)
|
||||||
|
for child in source_root.iterdir():
|
||||||
|
if child.name not in wanted:
|
||||||
|
if child.is_symlink() or child.is_file():
|
||||||
|
child.unlink()
|
||||||
|
elif child.is_dir():
|
||||||
|
shutil.rmtree(child)
|
||||||
|
|
||||||
|
for addon in addons:
|
||||||
|
dst = source_root / addon
|
||||||
|
src = root / addon
|
||||||
|
if dst.is_symlink() and dst.resolve() == src.resolve():
|
||||||
|
continue
|
||||||
|
if dst.exists() or dst.is_symlink():
|
||||||
|
if dst.is_dir() and not dst.is_symlink():
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
else:
|
||||||
|
dst.unlink()
|
||||||
|
dst.symlink_to(src, target_is_directory=True)
|
||||||
|
|
||||||
|
return source_root
|
||||||
|
|
||||||
|
|
||||||
|
def run_pboproject(addon: str, source_root: Path, output_dir: Path) -> None:
|
||||||
|
addon_source = source_root / addon
|
||||||
|
if not addon_source.is_dir():
|
||||||
|
raise RuntimeError(f"Mikero source directory not found: {addon_source}")
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
pboproject = shutil.which("pboproject") or shutil.which("pboProject")
|
||||||
|
makepbo = shutil.which("makepbo") or shutil.which("MakePbo")
|
||||||
|
|
||||||
|
if pboproject:
|
||||||
|
cmd = [
|
||||||
|
pboproject,
|
||||||
|
"-P",
|
||||||
|
str(addon_source),
|
||||||
|
f"+M={output_dir}",
|
||||||
|
"+O" if addon in obfuschatedList else "-O",
|
||||||
|
]
|
||||||
|
elif makepbo:
|
||||||
|
cmd = [
|
||||||
|
makepbo,
|
||||||
|
"-P",
|
||||||
|
f"+@=braf\\{addon}",
|
||||||
|
str(addon_source),
|
||||||
|
str(output_dir / f"{addon}.pbo"),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Neither pboProject nor makepbo was found in PATH")
|
||||||
|
|
||||||
|
print(f"::group::Building {addon}")
|
||||||
|
print(" ".join(cmd))
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
finally:
|
||||||
|
print("::endgroup::")
|
||||||
|
|
||||||
|
|
||||||
|
def promote_build_output(addon: str, build_dir: Path, cache_addons_dir: Path) -> None:
|
||||||
|
pbo = build_dir / f"{addon}.pbo"
|
||||||
|
if not pbo.is_file():
|
||||||
|
candidates = sorted(build_dir.glob("*.pbo"))
|
||||||
|
candidate_names = ", ".join(path.name for path in candidates) or "none"
|
||||||
|
raise RuntimeError(f"Expected {pbo.name} from pboProject; found: {candidate_names}")
|
||||||
|
|
||||||
|
cache_addons_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
remove_cached_addon(cache_addons_dir, addon)
|
||||||
|
|
||||||
|
for path in sorted(build_dir.glob(f"{addon}.pbo*")):
|
||||||
|
if path.is_file():
|
||||||
|
shutil.copy2(path, cache_addons_dir / path.name)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mod_assets(mod_cpp: Path) -> list[Path]:
|
||||||
|
if not mod_cpp.is_file():
|
||||||
|
return []
|
||||||
|
|
||||||
|
text = mod_cpp.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
assets: list[Path] = []
|
||||||
|
for field in MOD_ASSET_FIELDS:
|
||||||
|
pattern = re.compile(rf"\b{re.escape(field)}\s*=\s*\"([^\"]*)\"", re.IGNORECASE)
|
||||||
|
for match in pattern.finditer(text):
|
||||||
|
value = match.group(1).strip()
|
||||||
|
if value:
|
||||||
|
assets.append(Path(value.replace("\\", "/")))
|
||||||
|
return assets
|
||||||
|
|
||||||
|
|
||||||
|
def copy_metadata(root: Path, stage_dir: Path) -> None:
|
||||||
|
for name in METADATA_FILES:
|
||||||
|
src = root / name
|
||||||
|
if not src.is_file():
|
||||||
|
raise RuntimeError(f"Required metadata file is missing: {name}")
|
||||||
|
shutil.copy2(src, stage_dir / name)
|
||||||
|
|
||||||
|
for rel_asset in parse_mod_assets(root / "mod.cpp"):
|
||||||
|
if rel_asset.is_absolute() or ".." in rel_asset.parts:
|
||||||
|
raise RuntimeError(f"Unsupported mod.cpp asset path: {rel_asset}")
|
||||||
|
src = root / rel_asset
|
||||||
|
if not src.is_file():
|
||||||
|
raise RuntimeError(f"mod.cpp references missing asset: {rel_asset}")
|
||||||
|
dst = stage_dir / rel_asset
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def stage_package(
|
||||||
|
root: Path,
|
||||||
|
stage_dir: Path,
|
||||||
|
cache_addons_dir: Path,
|
||||||
|
addons: list[str],
|
||||||
|
) -> None:
|
||||||
|
if stage_dir.exists():
|
||||||
|
shutil.rmtree(stage_dir)
|
||||||
|
addons_dir = stage_dir / "addons"
|
||||||
|
addons_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
copy_metadata(root, stage_dir)
|
||||||
|
|
||||||
|
for addon in addons:
|
||||||
|
files = addon_cache_files(cache_addons_dir, addon)
|
||||||
|
if not any(path.name == f"{addon}.pbo" for path in files):
|
||||||
|
raise RuntimeError(f"Missing cached PBO for {addon}")
|
||||||
|
for path in files:
|
||||||
|
shutil.copy2(path, addons_dir / path.name)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
root = repo_root()
|
||||||
|
|
||||||
|
if not args.cache_dir:
|
||||||
|
print("ERROR: --cache-dir or BRAF_CI_CACHE_DIR is required", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
cache_dir = Path(args.cache_dir).resolve()
|
||||||
|
cache_addons_dir = cache_dir / "addons"
|
||||||
|
manifest_path = cache_dir / "manifest.json"
|
||||||
|
stage_dir = (root / args.stage_dir).resolve()
|
||||||
|
|
||||||
|
addons = buildable_addons(root)
|
||||||
|
if args.source_root:
|
||||||
|
source_root = Path(args.source_root).resolve()
|
||||||
|
elif args.dry_run:
|
||||||
|
source_root = (cache_dir / "mikero-source" / "braf").resolve()
|
||||||
|
else:
|
||||||
|
source_root = prepare_mikero_source_root(root, cache_dir, addons).resolve()
|
||||||
|
|
||||||
|
current_hashes = {addon: addon_digest(root / addon) for addon in addons}
|
||||||
|
manifest = load_manifest(manifest_path)
|
||||||
|
manifest_addons = manifest.setdefault("addons", {})
|
||||||
|
|
||||||
|
removed_addons = sorted(set(manifest_addons) - set(addons))
|
||||||
|
for addon in removed_addons:
|
||||||
|
print(f"Removing stale addon from cache manifest: {addon}")
|
||||||
|
if not args.dry_run:
|
||||||
|
remove_cached_addon(cache_addons_dir, addon)
|
||||||
|
manifest_addons.pop(addon, None)
|
||||||
|
|
||||||
|
to_build: list[str] = []
|
||||||
|
for addon in addons:
|
||||||
|
cached_hash = manifest_addons.get(addon, {}).get("hash")
|
||||||
|
if (
|
||||||
|
args.force_rebuild
|
||||||
|
or cached_hash != current_hashes[addon]
|
||||||
|
or not cached_pbo_exists(cache_addons_dir, addon)
|
||||||
|
):
|
||||||
|
to_build.append(addon)
|
||||||
|
|
||||||
|
print(f"Buildable addons: {len(addons)}")
|
||||||
|
print(f"Addons to build: {', '.join(to_build) if to_build else 'none'}")
|
||||||
|
print(f"Cache dir: {cache_dir}")
|
||||||
|
print(f"Stage dir: {stage_dir}")
|
||||||
|
print(f"Mikero source root: {source_root}")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for addon in to_build:
|
||||||
|
with TemporaryDirectory(prefix=f"{addon}-", dir=str(cache_dir)) as tmp:
|
||||||
|
build_dir = Path(tmp)
|
||||||
|
run_pboproject(addon, source_root, build_dir)
|
||||||
|
promote_build_output(addon, build_dir, cache_addons_dir)
|
||||||
|
manifest_addons[addon] = {"hash": current_hashes[addon]}
|
||||||
|
write_manifest(manifest_path, manifest)
|
||||||
|
|
||||||
|
for addon in addons:
|
||||||
|
if not cached_pbo_exists(cache_addons_dir, addon):
|
||||||
|
raise RuntimeError(f"Cache does not contain required PBO after build: {addon}")
|
||||||
|
|
||||||
|
stage_package(root, stage_dir, cache_addons_dir, addons)
|
||||||
|
write_manifest(manifest_path, manifest)
|
||||||
|
|
||||||
|
print(f"Staged {len(addons)} addons at {stage_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
protocol = 1;
|
||||||
|
name = "Brazilian Armed Forces (BRAF) Mod";
|
||||||
|
author = "BRAF Team";
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
name = "Brazilian Armed Forces (BRAF) Mod";
|
||||||
|
author = "BRAF Team";
|
||||||
|
tooltip = "Brazilian Armed Forces (BRAF) Mod";
|
||||||
|
tooltipOwned = "Brazilian Armed Forces (BRAF) Mod";
|
||||||
|
overview = "Modificacao para Arma 3 inspirada nas Forcas Armadas Brasileiras.";
|
||||||
|
actionName = "GitHub";
|
||||||
|
action = "";
|
||||||
|
picture = "";
|
||||||
|
logo = "";
|
||||||
|
logoOver = "";
|
||||||
|
hideName = 0;
|
||||||
|
hidePicture = 0;
|
||||||
Reference in New Issue
Block a user