Cleanup part one

This commit is contained in:
2025-09-19 18:51:25 +02:00
parent c2a93cbd86
commit bf6e1050d9
14 changed files with 0 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import os
import sys
import zipfile
import tempfile
import shutil
import subprocess
from datetime import datetime, timezone
from collections import defaultdict
def eprint(*a, **kw):
print(*a, file=sys.stderr, **kw)
def is_git_repo(path: str) -> bool:
return os.path.isdir(os.path.join(path, ".git"))
def run(cmd, cwd=None, env=None, check=True, capture=True):
result = subprocess.run(
cmd,
cwd=cwd,
env=env,
text=True,
capture_output=capture,
)
if check and result.returncode != 0:
eprint(f"[BŁĄD] Komenda nie powiodła się: {' '.join(cmd)}")
if capture:
eprint(result.stdout)
eprint(result.stderr)
sys.exit(result.returncode)
return result
def list_zip_files(zip_dir: str):
zips = []
for name in os.listdir(zip_dir):
if not name.lower().endswith(".zip"):
continue
path = os.path.join(zip_dir, name)
try:
st = os.stat(path)
except FileNotFoundError:
continue
zips.append((st.st_mtime, path))
zips.sort(key=lambda x: x[0]) # najstarsze -> najnowsze
return [p for _, p in zips]
def find_module_json(root: str):
"""Zwraca listę absolutnych ścieżek do plików module.json (rekurencyjnie), pomija .git."""
matches = []
for dirpath, dirnames, filenames in os.walk(root):
if ".git" in dirnames:
dirnames.remove(".git")
if "module.json" in filenames:
matches.append(os.path.join(dirpath, "module.json"))
return matches
def ensure_subdir(repo_root: str, subdir: str) -> str:
"""Zwraca absolutną ścieżkę docelowego podkatalogu w repo, tworząc go jeśli trzeba.
Zabezpiecza przed wychodzeniem poza repo (..)."""
target = repo_root
if subdir:
target = os.path.abspath(os.path.join(repo_root, subdir))
repo_root_abs = os.path.abspath(repo_root)
if os.path.commonpath([target, repo_root_abs]) != repo_root_abs:
eprint(f"[BŁĄD] Niedozwolona ścieżka podkatalogu: {subdir}")
sys.exit(1)
os.makedirs(target, exist_ok=True)
return target
def build_repo_index(repo_root: str):
"""Mapa: basename -> [abspathy] (bez .git)."""
index = defaultdict(list)
for dirpath, dirnames, filenames in os.walk(repo_root):
if ".git" in dirnames:
dirnames.remove(".git")
for f in filenames:
index[f].append(os.path.join(dirpath, f))
return index
def git_mv_or_rename(repo_root: str, src_abs: str, dest_abs: str):
"""Przenosi plik używając git mv, a w razie potrzeby zwykłego move. Tworzy katalog docelowy."""
if os.path.abspath(src_abs) == os.path.abspath(dest_abs):
return
os.makedirs(os.path.dirname(dest_abs), exist_ok=True)
rel_src = os.path.relpath(src_abs, repo_root)
rel_dest = os.path.relpath(dest_abs, repo_root)
# Spróbuj git mv (utrzymanie historii)
res = subprocess.run(["git", "mv", "-f", rel_src, rel_dest], cwd=repo_root)
if res.returncode != 0:
# Jeśli plik nie był w git lub inny problem — zwykłe przeniesienie
shutil.move(src_abs, dest_abs)
def mirror_zip_into_target(zip_root: str, target_dir: str, repo_root: str, dry_run: bool):
"""Odwzorowuje strukturę zip_root wewnątrz target_dir.
- Jeśli plik o tej samej nazwie istnieje w repo w innej ścieżce i dest nie istnieje -> przenosimy (git mv).
- W przeciwnym razie kopiujemy (nadpisując).
"""
repo_index = build_repo_index(repo_root)
for dirpath, dirnames, filenames in os.walk(zip_root):
rel_dir = os.path.relpath(dirpath, zip_root)
dest_dir = target_dir if rel_dir in (".", "") else os.path.join(target_dir, rel_dir)
if dry_run:
print(f"[PLAN] Utworzę katalog: {os.path.relpath(dest_dir, repo_root)}")
else:
os.makedirs(dest_dir, exist_ok=True)
for fn in filenames:
src = os.path.join(dirpath, fn)
dest = os.path.join(dest_dir, fn)
# 1) Unikaj duplikatów: jeśli dest nie istnieje, a w repo jest dokładnie jeden kandydat o tej samej nazwie -> przenieś
if not os.path.exists(dest):
candidates = [p for p in repo_index.get(fn, []) if os.path.abspath(p) != os.path.abspath(dest)]
# Odfiltruj ewentualne kandydaty wewnątrz .git (i tak ich nie było dodanych)
candidates = [p for p in candidates if "/.git/" not in p.replace("\\", "/")]
if len(candidates) == 1:
prev = candidates[0]
rel_prev = os.path.relpath(prev, repo_root)
rel_new = os.path.relpath(dest, repo_root)
if dry_run:
print(f"[MOVE] {rel_prev} -> {rel_new}")
else:
git_mv_or_rename(repo_root, prev, dest)
# aktualizuj indeks
repo_index[fn] = [dest if p == prev else p for p in repo_index.get(fn, [])]
elif len(candidates) > 1:
# Nie zgadujemy, żeby nie zrobić złej migracji — kopiujemy i ostrzegamy
print(f"[INFO] Wiele kandydatów o nazwie '{fn}' w repo ({len(candidates)}). "
f"Kopiuję do {os.path.relpath(dest, repo_root)} (bez przenoszenia).")
# 2) Nadpisz/utwórz plik zawartością z ZIPa (repo ma odzwierciedlać ZIP)
if dry_run:
print(f"[COPY] {os.path.relpath(src, zip_root)} -> {os.path.relpath(dest, repo_root)}")
else:
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.copy2(src, dest)
# 3) Upewnij się, że indeks widzi plik w nowym miejscu (dla kolejnych kolizji)
if dest not in repo_index.get(fn, []):
repo_index[fn].append(dest)
def git_has_staged_changes(repo: str) -> bool:
res = subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=repo)
return res.returncode == 1 # 1 = są zmiany; 0 = brak
def git_is_clean(repo: str) -> bool:
res = run(["git", "status", "--porcelain"], cwd=repo, capture=True, check=False)
return res.stdout.strip() == ""
def main():
parser = argparse.ArgumentParser(
description="Import archiwów ZIP do repozytorium git (od najstarszego), z commitami po każdym ZIPie."
)
parser.add_argument("--zip-dir", required=True, help="Katalog z plikami .zip")
parser.add_argument("--repo", required=True, help="Katalog repozytorium git (cel)")
parser.add_argument(
"--subdir",
default="",
help="Opcjonalny podkatalog w repo, do którego mają trafić pliki (utworzony jeśli nie istnieje).",
)
parser.add_argument(
"--allow-dirty",
action="store_true",
help="Pozwól działać, nawet jeśli repo nie jest czyste (domyślnie: wymagana czystość).",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Tylko pokaż plan (bez rozpakowywania i commitów).",
)
args = parser.parse_args()
zip_dir = os.path.abspath(args.zip_dir)
repo = os.path.abspath(args.repo)
subdir = args.subdir.strip()
if not os.path.isdir(zip_dir):
eprint(f"[BŁĄD] Nie ma katalogu: {zip_dir}")
sys.exit(1)
if not os.path.isdir(repo):
eprint(f"[BŁĄD] Nie ma katalogu repozytorium: {repo}")
sys.exit(1)
if not is_git_repo(repo):
eprint(f"[BŁĄD] {repo} nie wygląda na repozytorium git (brak .git).")
sys.exit(1)
target_dir = ensure_subdir(repo, subdir)
if not args.allow_dirty and not git_is_clean(repo):
eprint("[BŁĄD] Repo nie jest czyste. Użyj --allow-dirty, jeśli chcesz mimo to kontynuować.")
sys.exit(1)
zips = list_zip_files(zip_dir)
if not zips:
print("[INFO] Brak plików .zip do przetworzenia.")
return
rel_target = os.path.relpath(target_dir, repo)
print(f"[INFO] Cel importu: {target_dir} (rel: {rel_target})")
print(f"[INFO] Znaleziono {len(zips)} archiwów .zip do przetworzenia (od najstarszego do najnowszego).")
for i, zip_path in enumerate(zips, start=1):
zip_name = os.path.basename(zip_path)
mtime = datetime.fromtimestamp(os.path.getmtime(zip_path), tz=timezone.utc)
mtime_iso = mtime.strftime("%Y-%m-%dT%H:%M:%SZ")
print(f"\n=== [{i}/{len(zips)}] {zip_name} (mtime: {mtime_iso}) ===")
if args.dry_run:
print(f"[DRY-RUN] Odtworzyłbym strukturę {zip_name} w '{rel_target}', "
f"z przenosinami plików o tej samej nazwie.")
continue
stage_dir = tempfile.mkdtemp(prefix="zipstage_")
try:
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(stage_dir)
except zipfile.BadZipFile:
eprint(f"[BŁĄD] Uszkodzony zip: {zip_name}. Pomijam.")
shutil.rmtree(stage_dir, ignore_errors=True)
continue
# Ustal punkt wejścia importu
matches = find_module_json(stage_dir)
if len(matches) == 1:
zip_root = os.path.dirname(matches[0])
rel = os.path.relpath(zip_root, stage_dir)
print(f"[OK] Wykryto module.json w '{rel}'. Odtwarzam strukturę tego katalogu.")
elif len(matches) == 0:
zip_root = stage_dir
print("[INFO] Brak module.json. Odtwarzam pełną strukturę ZIPa w repo (merge z istniejącymi katalogami).")
else:
# wiele module.json — to ryzykowne; pozostawiamy do ręcznej interwencji
print("[UWAGA] Wykryto wiele plików module.json w archiwum. "
"Ta sytuacja jest niejednoznaczna — pomijam ten ZIP.")
shutil.rmtree(stage_dir, ignore_errors=True)
continue
# Mirror + przenosiny
mirror_zip_into_target(zip_root, target_dir, repo, dry_run=False)
# Git add + commit (jeśli są zmiany) commit z korzenia repo
run(["git", "add", "-A"], cwd=repo)
if git_has_staged_changes(repo):
msg = f"Import {zip_name} do {rel_target} ({mtime_iso})"
env = os.environ.copy()
env["GIT_AUTHOR_DATE"] = mtime_iso
env["GIT_COMMITTER_DATE"] = mtime_iso
run(["git", "commit", "-m", msg], cwd=repo, env=env)
print(f"[OK] Commit: {msg}")
else:
print("[INFO] Brak zmian po rozpakowaniu — pomijam commit.")
shutil.rmtree(stage_dir, ignore_errors=True)
print("\n[ZAKOŃCZONE] Przetwarzanie archiwów .zip zakończone.")
print(f"[INFO] Pliki zostały odzwierciedlone w: {target_dir}")
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
import json, sys
BLURB = {
"Jericho-class Pilgrim Vessel": "Immense conversions for pilgrim passages and reliquary cargo. Cavernous holds and chapels; range and capacity over armour and speed.",
"Vagabond-class Merchant Trader": "Modular cargobays and adaptable fittings; a chartist workhorse. Simple to keep running in frontier yards; heavily customised.",
"Hazeroth-class Privateer": "Knife-fast raider with reinforced internals and stripped armour. Favoured for ambushes and commerce raiding.",
"Havoc-class Merchant Raider": "Hybrid carrier-raider balancing battery weight with cargo. Less protected than Navy hulls but brutal on traffic.",
"Sword-class Frigate": "Reliable escort with accurate dorsal lasers and stout drives. Squadrons screen convoys and prosecute pirates.",
"Tempest-class Strike Frigate": "Close-action brawler for boardings and short-range gunnery. Reinforced prow and over-tuned drives.",
"Dauntless-class Light Cruiser": "Versatile patrol cruiser—good acceleration and flexible refits. Often leads Passage Watch patrols.",
"Lunar-class Cruiser": "Archetypal line cruiser—balanced, dependable, easily refitted. Backbone of many squadrons.",
"Cobra-class Destroyer": "Torpedo raider hunting in packs; strikes, then disengages to reload.",
"Falchion-class Frigate": "Modernised frigate with improved drives for frontier scouting.",
"Turbulent-class Heavy Frigate": "Strengthened frame and broader arcs; trades agility for staying power.",
"Defiant-class Light Cruiser (Carrier)": "Light carrier with significant attack craft; excellent for recon and strikes.",
"Universe-class Mass Conveyor": "Super-massive hauler for colony loads and crusade materiel; logistics linchpin.",
"Goliath-class Factory Ship": "Roving manufactorum and refinery; long autonomous deployments.",
"Conquest-class Star Galleon": "Heavily built merchant galleon with strong batteries and hull.",
"Orion-class Star Clipper": "Fast passenger/light-cargo clipper for courier missions and risky runs.",
"Secutor-class Monitor-Cruiser": "Mechanicus monitor with powerful dorsals and redundant shielding; anchors actions.",
"Lathe-class Monitor-Cruiser": "Lathes-forged variant emphasising resilient sanctified systems.",
"Tyrant-class Cruiser": "Macrobattery-focused cruiser optimised for crushing broadsides.",
"Cobra-class Destroyer (Renegade)": "Ex-Navy Cobra in privateer hands with non-standard prow refits."
}
path = "packs/wg-voidship-hulls.db"
out = []
with open(path,"r",encoding="utf-8") as f:
for line in f:
if not line.strip(): continue
d = json.loads(line)
if d.get("name") in BLURB:
d.setdefault("system", {})
d["system"]["notes"] = BLURB[d["name"]]
out.append(json.dumps(d, ensure_ascii=False))
with open(path,"w",encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
print("Patched:", path, "entries:", len(out))
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Patchuje:
1) wg-voidship-hulls.db -> base.power = 0 (dla wszystkich kadłubów)
2) wg-voidship-items.db -> reaktory: use.power=0, mods.power wg subtype;
aux-plasma: use.power=0;
dopisuje 4 reaktory jeśli brak.
Uruchom w katalogu z plikami .db
"""
import json, shutil
from pathlib import Path
HULLS_FN = Path("wg-voidship-hulls.db")
ITEMS_FN = Path("wg-voidship-items.db")
REACTOR_POWER_BY_SUBTYPE = {
"escort": 40,
"frigate": 45,
"light-cruiser": 55,
"cruiser": 60,
}
# 4 reaktory „wzorcowe” do dopisania, jeśli brak:
REACTOR_TEMPLATES = [
{
"_id": "ITEMREACTCRU0001",
"name": "Plasma Reactor (Cruiser) — Mars-Pattern",
"type": "gear",
"img": "icons/svg/energy.svg",
"flags": {
"wg-voidships-builder": {
"voidOnly": True,
"component": {"kind": "reactor", "subtype": "cruiser", "tags": ["reactor", "plasma"]}
}
},
"system": {
"description": "Standardowy reaktor plazmowy klasy krążownik (Mars-Pattern). Generuje moc dla wszystkich systemów okrętu.",
"voidship": {
"slot": "Internal",
"use": {"space": 20, "power": 0},
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 60}
}
},
"effects": []
},
{
"_id": "ITEMREACTLCR0001",
"name": "Plasma Reactor (Light Cruiser) — Mars-Pattern",
"type": "gear",
"img": "icons/svg/energy.svg",
"flags": {
"wg-voidships-builder": {
"voidOnly": True,
"component": {"kind": "reactor", "subtype": "light-cruiser", "tags": ["reactor", "plasma"]}
}
},
"system": {
"description": "Reaktor plazmowy dla lekkich krążowników (Mars-Pattern).",
"voidship": {
"slot": "Internal",
"use": {"space": 18, "power": 0},
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 55}
}
},
"effects": []
},
{
"_id": "ITEMREACTFRI0001",
"name": "Plasma Reactor (Frigate) — Jovian-Pattern",
"type": "gear",
"img": "icons/svg/energy.svg",
"flags": {
"wg-voidships-builder": {
"voidOnly": True,
"component": {"kind": "reactor", "subtype": "frigate", "tags": ["reactor", "plasma"]}
}
},
"system": {
"description": "Reaktor plazmowy okrętów klasy fregata (Jovian-Pattern).",
"voidship": {
"slot": "Internal",
"use": {"space": 15, "power": 0},
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 45}
}
},
"effects": []
},
{
"_id": "ITEMREACTESC0001",
"name": "Plasma Reactor (Escort) — Jovian-Pattern",
"type": "gear",
"img": "icons/svg/energy.svg",
"flags": {
"wg-voidships-builder": {
"voidOnly": True,
"component": {"kind": "reactor", "subtype": "escort", "tags": ["reactor", "plasma"]}
}
},
"system": {
"description": "Lżejszy reaktor plazmowy dla eskort.",
"voidship": {
"slot": "Internal",
"use": {"space": 12, "power": 0},
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 40}
}
},
"effects": []
},
]
def read_ndjson(path: Path):
if not path.exists():
print(f"[WARN] Brak pliku: {path.name}")
return []
rows = []
with path.open("r", encoding="utf-8") as f:
for i, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except Exception as e:
print(f"[WARN] {path.name}:{i}: nie mogę zdekodować JSON: {e}")
return rows
def write_ndjson(path: Path, rows):
tmp = path.with_suffix(path.suffix + ".patched")
with tmp.open("w", encoding="utf-8") as f:
for obj in rows:
f.write(json.dumps(obj, ensure_ascii=False, separators=(",", ":")) + "\n")
tmp.replace(path)
def ensure_backup(path: Path):
bak = path.with_suffix(path.suffix + ".bak")
if path.exists() and not bak.exists():
shutil.copy2(path, bak)
print(f"[OK] Backup -> {bak.name}")
def patch_hulls():
if not HULLS_FN.exists():
print(f"[INFO] Pomijam hulls (brak {HULLS_FN.name})")
return
ensure_backup(HULLS_FN)
rows = read_ndjson(HULLS_FN)
patched = 0
for o in rows:
flags = o.setdefault("flags", {}).setdefault("wg-voidships-builder", {})
base = flags.setdefault("base", {})
if base.get("power", None) != 0:
base["power"] = 0
patched += 1
write_ndjson(HULLS_FN, rows)
print(f"[OK] Hulls: ustawiono base.power=0 w {patched} rekordach ({len(rows)} linii)")
def has_tag(flags_obj, tag: str) -> bool:
try:
tags = flags_obj["wg-voidships-builder"]["component"]["tags"]
return isinstance(tags, list) and tag in tags
except Exception:
return False
def patch_items():
if not ITEMS_FN.exists():
print(f"[INFO] Pomijam items (brak {ITEMS_FN.name})")
return
ensure_backup(ITEMS_FN)
rows = read_ndjson(ITEMS_FN)
existing_ids = set()
patched_reactors = 0
patched_aux = 0
for o in rows:
existing_ids.add(o.get("_id", ""))
flags = o.get("flags", {})
comp = flags.get("wg-voidships-builder", {}).get("component", {})
kind = comp.get("kind")
subtype = comp.get("subtype")
# Upewnij się, że ścieżka system.voidship.use/mods istnieje
sys = o.setdefault("system", {})
vsh = sys.setdefault("voidship", {})
use = vsh.setdefault("use", {})
mods = vsh.setdefault("mods", {})
# Auxiliary plasma (po tagu)
if has_tag(flags, "aux-plasma"):
if use.get("power") != 0:
use["power"] = 0
patched_aux += 1
# Reaktory
if kind == "reactor":
if use.get("power") != 0:
use["power"] = 0
if subtype in REACTOR_POWER_BY_SUBTYPE:
pow_val = REACTOR_POWER_BY_SUBTYPE[subtype]
if mods.get("power") != pow_val:
mods["power"] = pow_val
patched_reactors += 1
# zadbaj o wypełnienie brakujących kluczy modów
for k in ["armour","integrity","manoeuvre","detection","speed","space"]:
mods.setdefault(k, 0)
# i sensowne sloty/space
vsh.setdefault("slot", "Internal")
use.setdefault("space", use.get("space", 0))
# Zapisz zmodyfikowane wiersze
write_ndjson(ITEMS_FN, rows)
print(f"[OK] Items: zaktualizowano reaktory: {patched_reactors}, aux-plasma: {patched_aux}")
# Dopisz brakujące 4 reaktory
to_append = [r for r in REACTOR_TEMPLATES if r["_id"] not in existing_ids]
if to_append:
with ITEMS_FN.open("a", encoding="utf-8") as f:
for obj in to_append:
f.write(json.dumps(obj, ensure_ascii=False, separators=(",", ":")) + "\n")
print(f"[OK] Items: dopisano brakujące reaktory: {[o['_id'] for o in to_append]}")
else:
print(f"[OK] Items: wszystkie reaktory już są — nic do dopisania.")
def main():
print("== WG Voidships Patch ==")
patch_hulls()
patch_items()
print("Gotowe. Zrestartuj/reloadnij Foundry i reimportuj kompendia, jeśli potrzeba.")
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
import numpy as np
import soundfile as sf
from scipy.signal import lfilter, fftconvolve
# Global Parameters
sample_rate = 44100
duration_seconds = 30
samples = duration_seconds * sample_rate
t = np.linspace(0, duration_seconds, samples, endpoint=False)
# === 1. Aggressive White Noise + Reverb ===
def aggressive_white_noise():
noise = np.random.normal(0, 0.07, samples)
# High-pass filter
b = [0.95, -0.95]
a = [1, -0.85]
filtered_noise = lfilter(b, a, noise)
# Reverb via impulse response (short decaying tail)
impulse_response = np.exp(-np.linspace(0, 1.5, int(0.3 * sample_rate)))
impulse_response = impulse_response * np.random.uniform(0.8, 1.2, impulse_response.shape)
reverb_noise = fftconvolve(filtered_noise, impulse_response, mode='full')[:samples]
return reverb_noise
white_noise = aggressive_white_noise()
# === 3. Metallic Sonar Ping with Echo ===
def generate_sonar_ping(start_time, freq=5000, ping_duration=0.82):
ping_t = np.linspace(0, ping_duration, int(sample_rate * ping_duration), endpoint=False)
# FM modulation for metallic texture
mod_freq = 1000 # Modulating frequency
fm_wave = np.sin(2 * np.pi * freq * ping_t + 5 * np.sin(2 * np.pi * mod_freq * ping_t))
envelope = np.exp(-8 * ping_t)
ping = fm_wave * envelope * 0.6
mod_freq = 4000 # Modulating frequency
fm_wave = np.sin(2 * np.pi * freq * ping_t + 5 * np.sin(2 * np.pi * mod_freq * ping_t))
envelope = np.exp(-8 * ping_t)
ping = fm_wave * envelope * 0.9
mod_freq = 7000 # Modulating frequency
fm_wave = np.sin(2 * np.pi * freq * ping_t + 5 * np.sin(2 * np.pi * mod_freq * ping_t))
envelope = np.exp(-8 * ping_t)
ping = fm_wave * envelope * 0.5
# Add echo (delay)
echo_delay = int(0.01 * sample_rate)
echo = np.pad(ping * 0.1, (echo_delay, 0), 'constant')[:len(ping)]
ping = ping + echo
echo_delay = int(0.06 * sample_rate)
echo = np.pad(ping * 0.1, (echo_delay, 0), 'constant')[:len(ping)]
ping = ping + echo
echo_delay = int(0.06 * sample_rate)
echo = np.pad(ping * 0.1, (echo_delay, 0), 'constant')[:len(ping)]
ping = ping + echo
echo_delay = int(0.1 * sample_rate)
echo = np.pad(ping * 0.1, (echo_delay, 0), 'constant')[:len(ping)]
ping = ping + echo
echo_delay = int(0.2 * sample_rate)
echo = np.pad(ping * 0.1, (echo_delay, 0), 'constant')[:len(ping)]
ping = ping + echo
sonar = np.zeros(samples)
start_sample = int(start_time * sample_rate)
end_sample = start_sample + len(ping)
if end_sample <= samples:
sonar[start_sample:end_sample] = ping
return sonar
sonar_total = np.zeros(samples)
np.random.seed(42)
ping_times = np.cumsum(np.random.uniform(0.5, 3.0, size=12))
ping_times = ping_times[ping_times < duration_seconds]
for pt in ping_times:
sonar_total += generate_sonar_ping(pt)
# === 4. Combine & Normalize ===
final_mix = 0.2*white_noise + 3.0*sonar_total
final_mix = final_mix / np.max(np.abs(final_mix))
# === 5. Export ===
file_path = 'test.wav'
sf.write(file_path, final_mix, sample_rate)
print(f"✅ Saved to: {file_path}")
+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
sync_foundry_module.py (z dry-run, argumentami CLI i preflightem git/module.json)
Kroki:
[PRE] W katalogu źródłowym:
- Wczytaj module.json, pobierz 'id' i 'version'
- Zwiększ ostatnią liczbę w 'version' o 1, zapisz module.json
- git add -A; git commit -m "sync: <id> v<version>"; git push
(w dry-run wyłącznie plan, bez modyfikacji i bez gita)
[2] Wyczyść zawartość docelowego modułu (z potwierdzeniem lub bez; w dry-run tylko lista)
[3] Skopiuj zawartość źródła do celu (w dry-run: plan)
Wymagania: Python 3.8+, git w PATH, uprawnienia do zapisu.
"""
from __future__ import annotations
import os
import sys
import shutil
import argparse
import json
import subprocess
from pathlib import Path
from typing import List, Optional, Tuple
# ==========================
# KONFIGURACJA (DOMYŚLNA)
# ==========================
TARGET_USER: str = "mtusz" # nazwa folderu w C:\Users\...
APPDATA_KIND: Optional[str] = None # None | "Local" | "LocalLow" | "Roaming"
TARGET_MODULES_DIR_OVERRIDE: Optional[str] = None # pełna ścieżka do ...\FoundryVTT\Data\modules
MODULE_SUBDIR_NAME: str = "wg-voidships-builder"
CONFIRM_DELETE: bool = True
SOURCE_BASE_DIR: str = r"C:\vtt_work"
RELATIVE_PATTERN: str = r"FoundryVTT\Data\modules"
DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia)
# ==========================
# KONIEC KONFIGURACJI
# ==========================
APPDATA_VARIANTS = ("Local", "LocalLow", "Roaming")
class AmbiguityError(RuntimeError):
pass
def norm(p: Path) -> Path:
return Path(os.path.normpath(str(p)))
def find_modules_dir_for_user(user: str,
relative_pattern: str,
appdata_kind: Optional[str]) -> Path:
base = Path(f"C:/Users/{user}/AppData")
if not base.exists():
raise FileNotFoundError(f"Nie znaleziono katalogu użytkownika: {norm(base)}")
candidates: List[Path] = []
if appdata_kind:
if appdata_kind not in APPDATA_VARIANTS:
raise ValueError(f"APPDATA_KIND musi być jednym z: {APPDATA_VARIANTS}, a nie '{appdata_kind}'")
p = base / appdata_kind / Path(relative_pattern)
if p.is_dir():
return norm(p)
else:
raise FileNotFoundError(
f"Nie znaleziono katalogu: {norm(p)} (dla APPDATA_KIND={appdata_kind})."
)
for kind in APPDATA_VARIANTS:
p = base / kind / Path(relative_pattern)
if p.is_dir():
candidates.append(p)
if not candidates:
raise FileNotFoundError(
"Nie znaleziono żadnego katalogu zgodnego z wzorcem.\n"
f"Sprawdź, czy istnieje któryś z poniższych:\n"
+ "\n".join(f" - {norm(base / kind / Path(relative_pattern))}" for kind in APPDATA_VARIANTS)
)
if len(candidates) == 1:
return norm(candidates[0])
suggestions = []
for p in candidates:
try:
kind = p.parts[p.parts.index("AppData") + 1]
except Exception:
kind = "??"
suggestions.append(f'* Ustaw APPDATA_KIND="{kind}" # trafi w: {norm(p)}')
suggestions.append(f'* Albo ustaw TARGET_MODULES_DIR_OVERRIDE="{norm(candidates[0])}" # pełny override')
raise AmbiguityError(
"Znaleziono wiele pasujących lokalizacji dla wzorca 'FoundryVTT\\Data\\modules':\n"
+ "\n".join(f" - {norm(c)}" for c in candidates)
+ "\n\nPropozycje ujednolicenia:\n" + "\n".join(suggestions)
)
def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def iter_dir_contents(dir_path: Path) -> List[Path]:
if not dir_path.exists():
return []
return list(dir_path.iterdir())
def list_recursive(dir_path: Path) -> List[Path]:
items: List[Path] = []
if not dir_path.exists():
return items
for root, dirs, files in os.walk(dir_path):
root_p = Path(root)
for d in dirs:
items.append(root_p / d)
for f in files:
items.append(root_p / f)
return items
def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]:
ensure_dir(dir_path)
files = 0
dirs = 0
to_delete = iter_dir_contents(dir_path)
if dry_run:
print("[DRY-RUN] Zawartość do skasowania:")
if not to_delete:
print(" (pusto)")
for child in to_delete:
tag = "[plik]" if child.is_file() else "[katalog]" if child.is_dir() else "[inne]"
print(f" {tag} {norm(child)}")
for child in to_delete:
if child.is_file() or child.is_symlink():
files += 1
elif child.is_dir():
dirs += 1
return files, dirs
for child in to_delete:
if child.is_file() or child.is_symlink():
files += 1
child.unlink(missing_ok=True)
elif child.is_dir():
dirs += 1
shutil.rmtree(child, ignore_errors=True)
return files, dirs
def plan_copy(src: Path, dst: Path) -> List[Tuple[Path, Path, str]]:
plan: List[Tuple[Path, Path, str]] = []
if not src.is_dir():
raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(src)}")
for root, dirs, files in os.walk(src):
root_p = Path(root)
rel_root = root_p.relative_to(src)
for d in dirs:
s = root_p / d
t = dst / rel_root / d
status = "overwrite" if t.exists() else "create"
plan.append((s, t, status))
for f in files:
s = root_p / f
t = dst / rel_root / f
status = "overwrite" if t.exists() else "create"
plan.append((s, t, status))
return plan
def copy_dir_contents(src: Path, dst: Path, dry_run: bool) -> Tuple[int, int]:
ensure_dir(dst)
plan = plan_copy(src, dst)
if dry_run:
print("[DRY-RUN] Plan kopiowania:")
if not plan:
print(" (brak plików do skopiowania)")
for s, t, status in plan:
kind = "[katalog]" if s.is_dir() else "[plik]"
print(f" {kind} {norm(s)} -> {norm(t)} ({status})")
files = sum(1 for s, _, _ in plan if s.is_file())
dirs = sum(1 for s, _, _ in plan if s.is_dir())
return files, dirs
files = 0
dirs = 0
for s, t, _ in plan:
if s.is_dir():
shutil.copytree(s, t, dirs_exist_ok=True)
dirs += 1
elif s.is_file():
ensure_dir(t.parent)
shutil.copy2(s, t)
files += 1
elif s.is_symlink():
target = s.resolve()
ensure_dir(t.parent)
if target.is_dir():
shutil.copytree(target, t, dirs_exist_ok=True)
dirs += 1
else:
shutil.copy2(target, t)
files += 1
else:
print(f"[OSTRZEŻENIE] Pominięto nieobsługiwany typ pliku: {norm(s)}")
return files, dirs
# ============ PRE-FLIGHT: module.json + git ============
def read_manifest(manifest_path: Path) -> dict:
if not manifest_path.is_file():
raise FileNotFoundError(f"Brak pliku manifestu: {norm(manifest_path)}")
try:
with manifest_path.open("r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Niepoprawny JSON w {norm(manifest_path)}: {e}") from e
def bump_version(last_version: str) -> str:
"""
Zwiększa o 1 ostatnią *liczbę* w wersji rozdzielanej kropkami.
Przykłady: 1 -> 2, 1.2 -> 1.3, 1.2.9 -> 1.2.10
"""
parts = last_version.strip().split(".")
if not parts:
raise ValueError(f"Niepoprawna wersja: '{last_version}'")
try:
n = int(parts[-1])
except ValueError:
raise ValueError(f"Ostatni segment wersji nie jest liczbą: '{last_version}'")
parts[-1] = str(n + 1)
return ".".join(parts)
def write_manifest(manifest_path: Path, data: dict) -> None:
tmp = manifest_path.with_suffix(".json.tmp")
with tmp.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
tmp.replace(manifest_path) # atomowa podmiana na tym samym FS
def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
"""
Wykonuje: git add -A; git commit -m "<msg>"; git push
W dry_run — tylko wyświetla plan.
"""
if dry_run:
print("[DRY-RUN] git add -A")
print(f"[DRY-RUN] git commit -m {commit_msg!r}")
print("[DRY-RUN] git push")
return
def run(cmd: List[str]) -> None:
r = subprocess.run(cmd, cwd=str(repo_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if r.returncode != 0:
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{r.stdout}")
else:
if r.stdout.strip():
print(r.stdout)
# add, commit (może nie utworzyć commita jeśli brak zmian), push
run(["git", "add", "-A"])
# commit powinien się powieść — jeśli nie ma zmian, chcemy FAIL
r = subprocess.run(["git", "commit", "-m", commit_msg], cwd=str(repo_dir),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if r.returncode != 0:
# typowy komunikat: "nothing to commit, working tree clean" -> traktujemy jako błąd wg wymagań
raise RuntimeError(f"Błąd podczas git commit:\n{r.stdout}")
else:
if r.stdout.strip():
print(r.stdout)
run(["git", "push"])
def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
"""
Wczytuje module.json, bumpuje wersję, zapisuje (jeśli nie dry_run),
i wykonuje git add/commit/push. Zwraca (module_id, new_version).
"""
manifest_path = src_dir / "module.json"
manifest = read_manifest(manifest_path)
if "id" not in manifest or "version" not in manifest:
raise KeyError(f"W {norm(manifest_path)} brakuje pola 'id' lub 'version'.")
module_id = str(manifest["id"])
old_version = str(manifest["version"])
new_version = bump_version(old_version)
print(f"[PRE] module.json: id={module_id}, version={old_version} -> {new_version}")
if dry_run:
print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.")
else:
manifest["version"] = new_version
write_manifest(manifest_path, manifest)
print(f"[PRE] Zapisano zaktualizowany {norm(manifest_path)}")
commit_msg = f"sync: {module_id} v{new_version}"
run_git_commands(src_dir, commit_msg, dry_run=False)
return module_id, new_version
# ================= CLI / MAIN =================
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Synchronizacja modułu FoundryVTT (preflight: module.json + git).")
p.add_argument("--user", dest="user", default=TARGET_USER,
help=f"Nazwa użytkownika Windows (domyślnie: {TARGET_USER})")
p.add_argument("--appdata-kind", dest="appdata_kind", choices=APPDATA_VARIANTS + (None,),
default=APPDATA_KIND,
help=f"Wymuś AppData wariant: {APPDATA_VARIANTS} lub brak (None). Domyślnie: {APPDATA_KIND}")
p.add_argument("--modules-dir-override", dest="modules_dir_override",
default=TARGET_MODULES_DIR_OVERRIDE,
help="Pełna ścieżka do ...\\FoundryVTT\\Data\\modules (pomija wyszukiwanie).")
p.add_argument("--module-subdir-name", dest="module_subdir_name",
default=MODULE_SUBDIR_NAME,
help=f"Nazwa podkatalogu modułu (domyślnie: {MODULE_SUBDIR_NAME})")
cd_group = p.add_mutually_exclusive_group()
cd_group.add_argument("--confirm-delete", dest="confirm_delete", action="store_true",
help="Włącz potwierdzenie kasowania (jeśli było wyłączone).")
cd_group.add_argument("--no-confirm-delete", dest="confirm_delete", action="store_false",
help="Wyłącz potwierdzenie kasowania (tryb bez pytania).")
p.set_defaults(confirm_delete=CONFIRM_DELETE)
p.add_argument("--source-base-dir", dest="source_base_dir",
default=SOURCE_BASE_DIR,
help=f"Katalog źródłowy (domyślnie: {SOURCE_BASE_DIR})")
p.add_argument("--relative-pattern", dest="relative_pattern",
default=RELATIVE_PATTERN,
help=f"Wzorzec relatywny od AppData (domyślnie: {RELATIVE_PATTERN})")
dr_group = p.add_mutually_exclusive_group()
dr_group.add_argument("--dry-run", dest="dry_run", action="store_true",
help="Tryb symulacji: tylko plan (bez modyfikacji i bez gita).")
dr_group.add_argument("--no-dry-run", dest="dry_run", action="store_false",
help="Tryb wykonawczy: wprowadza zmiany i robi git.")
p.set_defaults(dry_run=DRY_RUN)
return p.parse_args()
def main() -> int:
args = parse_args()
user = args.user
appdata_kind = args.appdata_kind if args.appdata_kind != "None" else None
modules_override = args.modules_dir_override
module_subdir = args.module_subdir_name
confirm_delete = args.confirm_delete
source_base = args.source_base_dir
relative_pattern = args.relative_pattern
dry_run = args.dry_run
# Ścieżki
src_dir = Path(source_base) / module_subdir
# [PRE] module.json + git w katalogu źródłowym
print(f"[PRE] Katalog źródłowy: {norm(src_dir)}")
preflight_source(src_dir, dry_run=dry_run)
# 1) Ustal katalog modules (cel)
if modules_override:
modules_dir = Path(modules_override)
if not modules_dir.is_dir():
raise FileNotFoundError(f"TARGET_MODULES_DIR_OVERRIDE wskazuje na nieistniejący katalog: {norm(modules_dir)}")
else:
modules_dir = find_modules_dir_for_user(user, relative_pattern, appdata_kind)
print(f"[INFO] Katalog modules: {norm(modules_dir)}")
target_module_dir = modules_dir / module_subdir
print(f"[INFO] Docelowy katalog modułu: {norm(target_module_dir)}")
# 2) Kasowanie zawartości (albo plan w dry-run)
if confirm_delete and not dry_run:
ans = input(f"Czy na pewno wyczyścić zawartość '{norm(target_module_dir)}'? [y/N]: ").strip().lower()
if ans not in ("y", "yes", "t", "tak"):
print("[INFO] Przerwano na życzenie użytkownika.")
return 0
elif confirm_delete and dry_run:
print("[INFO] (dry-run) Pytanie o potwierdzenie pomijane — tryb symulacji.")
print("[AKCJA] Czyszczenie zawartości katalogu modułu..." if not dry_run else "[SYMULACJA] Plan czyszczenia katalogu modułu...")
del_files, del_dirs = clear_dir_contents(target_module_dir, dry_run=dry_run)
print(f"[INFO] {'Do usunięcia' if dry_run else 'Usunięto'}: plików={del_files}, katalogów={del_dirs}")
# 3) Kopiowanie (albo plan kopiowania)
print("[AKCJA] Kopiowanie zawartości..." if not dry_run else "[SYMULACJA] Plan kopiowania...")
cp_files, cp_dirs = copy_dir_contents(src_dir, target_module_dir, dry_run=dry_run)
print(f"[INFO] {'Do skopiowania' if dry_run else 'Skopiowano'}: plików={cp_files}, katalogów={cp_dirs}")
print("[SUKCES] " + ("Symulacja zakończona." if dry_run else "Zakończono synchronizację."))
print(f" Źródło: {norm(src_dir)}")
print(f" Cel: {norm(target_module_dir)}")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except AmbiguityError as e:
print("\n[NIERÓWNOZNACZNOŚĆ]\n" + str(e), file=sys.stderr)
sys.exit(2)
except Exception as e:
print("\n[BŁĄD]\n" + str(e), file=sys.stderr)
sys.exit(1)