chore(history): squash 59 commit(s) from 2026-09-23

- refactor(hud/weather/render): Scoreboard 931→240 + Rain 903→159 + UiMeshGeometry 956→99 (8.5.2)
- refactor(chams): Chams 985 → 129 + 4×≤195 (8.5.2)
- refactor(module): Module 1031 → 139 + 4×≤134 (8.5.2)
- refactor(tooltips): BetterTooltips 960 → 181 + 4×≤199 (8.5.2)
- refactor(clickgui): ClickGuiPickerState 970→166 + ModulesMenuScreen 966→145 (8.5.2)
- refactor(clickgui): CombatProtocolHeuristicsEditorState 932→79 + 4×≤192 (8.5.2)
- refactor(hud): CompactHudStatModel 894 → 125 + 4×≤175 (8.5.2)
- refactor(iris): ShaderPatchCompiler 843→66 + 4×≤174 (8.5.2)
- refactor(tab): CustomTabList 834→136 + 4×≤166 (8.5.2)
- refactor(relations): OnlineRelationPlayerPickerComponent 837→74 + 4×≤192 (8.5.2)
- refactor(hits): HitEffect 770→120 + 4×≤176 (8.5.2)
- refactor(predict): Predictions 815→150 + 4×≤195 (8.5.2)
- refactor(config): ConfigProfilesComponent 799→125 + 4×≤195 (8.5.2)
- refactor(sim): PlayerMovementSimulation 757→157 + 4×≤194 (8.5.2)
- refactor(predict): ProjectilePuncher 747→104 + 3×≤134 (8.5.2)
- refactor(crosshair): Crosshair 743→143 + 4×≤167 (8.5.2)
- refactor(mediaplayer): MediaPlayer 743→174 + 4×≤196 (8.5.2)
- refactor(hud): Itemizer 699→193 + Cooldowns 707→151 (8.5.2)
- refactor(hud): Potions 617→176 + 4×≤166 (8.5.2)
- refactor(hud): Admins 702→118 + 4×≤138 (8.5.2)
- refactor(hud): Keybinds 611→157 + 4×≤160 (8.5.2)
- refactor(hud): ModuleList 499→181 + 3×≤150 (8.5.2)
- refactor(hud): Radar 392→161 + 2×≤124 (8.5.2)
- refactor(hud): Fps 309→168 + 3×≤107 (8.5.2)
- refactor(hud): Memory 304→151 + 3×≤144 (8.5.2)
- refactor(hud): Tps 339→154 + 3×≤143 (8.5.2)
- refactor(hud): Ping 309→145 + 3×≤125 (8.5.2)
- refactor(hud): SpeedBps 334→156 + 3×≤142 (8.5.2)
- refactor(hud): GameTime 314→150 + 3×≤127 (8.5.2)
- refactor(hud): SystemTime 300→136 + 3×≤130 (8.5.2)
- refactor(hud): Coordinates 337→174 + 2×≤172 (8.5.2)
- refactor(hud): Armor 324→162 + 2×≤148 (8.5.2)
- refactor(hud): Inventory 476→153 + 3×≤181 (8.5.2)
- docs(todo): обновлён 8.5.2 — отмечены готовые гиганты (BetterButtons/InventorySwap/Svg/Темы/WorldParticles/Module/Chams и хвост)
- docs(api): папка combatant-client-26.2/docs + addons.md (API можно ломать при рефакторе)
- refactor(visuals): BlockHighlight 692→271 + 4×≤200 (8.5.2)
- refactor(mixins): GameRendererMixin 830→344 (хуки) + 4 хелпера (8.5.2)
- refactor(visuals): WorldParticles 821→260 + 6×≤200, режимы добиты (8.5.2)
- docs(todo): 8.5.2 — WorldParticles/GameRendererMixin/BlockHighlight отмечены ГОТОВО
- refactor(render): MeshBuilder 738→447 (фасад write-API) + 3 хелпера, FrameStats снесён (8.5.2)
- docs(todo): Фаза 10 — дизайн платформы (аккаунты/конфиги/аватарки), backlog RPC-чата
- docs(todo): Подсистема 3 — Аддоны 2.0 (скрипты/версии) + витрина аддонов + админка
- docs(todo): Подсистема 2 — RPC-чат/друзья/виджеты-телеметрия + рейт-лимиты, доп. правки Подсистемы 3
- chore: переименование combatant-client-26.2 -> mod, добавлены backend/ и frontend/
- docs(backend): implementation plan for accounts-service (Подсистема 1, часть 1)
- chore: пересоздать backend/frontend через cargo new и bun create vite + tailwind; убрать таблицу компонентов из TODO.md
- docs(backend): edition 2024 в плане вместо 2021
- chore: обновить версии до реально актуальных (проверено компиляцией)
- docs: правила платформы (лимит 250 строк, антипаттерны backend/frontend по итогам ресёрча) + frontend/ARCHITECTURE.md
- docs: добавить правило ≤4 файла на папку в правила платформы (backend/frontend)
- docs: commit messages in English from now on
- chore: gitignore .superpowers/ scratch directory
- docs: commit messages in English from now on (mod)
- feat(backend): scaffold accounts-service with health check
- chore(backend): remove target/ build artifacts from git, add gitignore
- chore: track docs/ in git (was accidentally gitignored, never committed)
- chore(backend): convert to Cargo workspace, plan full microservice layout
- feat(backend): accounts/avatars/device_links schema + migration
- feat(backend): argon2id password hashing
This commit is contained in:
loki5512344 2026-09-23 22:38:08 +02:00
parent ec10395511
commit 9d08fa910a
2239 changed files with 35265 additions and 28110 deletions

View file

@ -0,0 +1,168 @@
param(
[string]$InputRoot = "src/main/resources/assets/combatant/svg",
[string]$OutRoot = "build/generated/resources/svgMsdf/assets/combatant/svg/msdf",
[int]$Dimension = 64,
[int]$PxRange = 4,
[string]$Type = "mtsdf",
[string]$PreprocessorClasspath = "build/classes/java/main",
[string]$JavaExe = "java",
[switch]$Strict
)
$ErrorActionPreference = "Stop"
$repoRoot = (Resolve-Path "${PSScriptRoot}\..\..").Path
function Resolve-RepoPath {
param([string]$Path)
if ([System.IO.Path]::IsPathRooted($Path)) {
return [System.IO.Path]::GetFullPath($Path)
}
return [System.IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
}
$inputRootPath = Resolve-RepoPath $InputRoot
$outRootPath = Resolve-RepoPath $OutRoot
$toolDir = Join-Path $repoRoot "tools/msdf"
$msdfgenPath = Join-Path $toolDir "msdfgen.exe"
$workRoot = Join-Path $repoRoot "build/tmp/svg-msdf"
function Get-Msdfgen {
if (Test-Path $msdfgenPath) { return $msdfgenPath }
$api = "https://api.github.com/repos/Chlumsky/msdfgen/releases/latest"
$release = Invoke-RestMethod -Uri $api -Headers @{ "User-Agent" = "CombatantSvgMsdfGen" }
$asset = $release.assets |
Where-Object { $_.name -match "win64" -and $_.name -match "\.zip$" -and $_.name -notmatch "openmp" } |
Select-Object -First 1
if (-not $asset) {
$asset = $release.assets |
Where-Object { $_.name -match "win" -and $_.name -match "\.zip$" -and $_.name -notmatch "openmp" } |
Select-Object -First 1
}
if (-not $asset) { throw "No Windows msdfgen zip found in latest release." }
New-Item -ItemType Directory -Force -Path $toolDir | Out-Null
$archivePath = Join-Path $toolDir $asset.name
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $archivePath -Headers @{ "User-Agent" = "CombatantSvgMsdfGen" }
$extractDir = Join-Path $toolDir ([System.IO.Path]::GetFileNameWithoutExtension($asset.name))
Expand-Archive -Path $archivePath -DestinationPath $extractDir -Force
$found = Get-ChildItem -Path $extractDir -Filter "msdfgen.exe" -Recurse | Select-Object -First 1
if (-not $found) { throw "msdfgen.exe not found after extraction." }
Copy-Item $found.FullName -Destination $msdfgenPath -Force
return $msdfgenPath
}
function Read-ViewBox {
param([string]$Path)
[xml]$xml = Get-Content -LiteralPath $Path -Raw
$root = $xml.DocumentElement
$viewBox = $root.GetAttribute("viewBox")
if ($viewBox) {
$parts = [regex]::Split($viewBox.Trim(), "[,\s]+") | Where-Object { $_ -ne "" }
if ($parts.Count -eq 4) {
return @{
X = [double]::Parse($parts[0], [Globalization.CultureInfo]::InvariantCulture)
Y = [double]::Parse($parts[1], [Globalization.CultureInfo]::InvariantCulture)
W = [double]::Parse($parts[2], [Globalization.CultureInfo]::InvariantCulture)
H = [double]::Parse($parts[3], [Globalization.CultureInfo]::InvariantCulture)
}
}
}
$w = $root.GetAttribute("width") -replace "[^0-9.+-]", ""
$h = $root.GetAttribute("height") -replace "[^0-9.+-]", ""
$width = if ($w) { [double]::Parse($w, [Globalization.CultureInfo]::InvariantCulture) } else { 24.0 }
$height = if ($h) { [double]::Parse($h, [Globalization.CultureInfo]::InvariantCulture) } else { 24.0 }
return @{ X = 0.0; Y = 0.0; W = $width; H = $height }
}
function Write-Metadata {
param(
[string]$Path,
[string]$SourceRelative,
[string]$TextureRelative,
[hashtable]$ViewBox
)
$metadata = [ordered]@{
type = $Type
width = $Dimension
height = $Dimension
pxRange = $PxRange
yOrigin = "top"
source = "combatant:svg/$SourceRelative"
texture = "combatant:svg/msdf/$TextureRelative"
viewBox = @($ViewBox.X, $ViewBox.Y, $ViewBox.W, $ViewBox.H)
}
$json = $metadata | ConvertTo-Json -Depth 4
[System.IO.File]::WriteAllText($Path, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
}
if (-not (Test-Path $inputRootPath)) {
Write-Host "SVG input root not found: $inputRootPath" -ForegroundColor Yellow
exit 0
}
try {
$msdfgen = Get-Msdfgen
} catch {
if ($Strict) { throw }
Write-Host "msdfgen unavailable; skipping SVG MSDF generation: $($_.Exception.Message)" -ForegroundColor Yellow
exit 0
}
if (Test-Path $outRootPath) {
Remove-Item -LiteralPath $outRootPath -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $outRootPath | Out-Null
New-Item -ItemType Directory -Force -Path $workRoot | Out-Null
$svgs = Get-ChildItem -Path $inputRootPath -Filter "*.svg" -Recurse
$inputRootFull = [System.IO.Path]::GetFullPath($inputRootPath).TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
$generated = 0
$skipped = 0
foreach ($svg in $svgs) {
$fileFull = [System.IO.Path]::GetFullPath($svg.FullName)
$relative = $fileFull.Substring($inputRootFull.Length).Replace('\', '/')
if ($relative.StartsWith("msdf/")) { continue }
$relativeNoExt = $relative.Substring(0, $relative.Length - 4)
$outPng = Join-Path $outRootPath ($relativeNoExt + ".png")
$outJson = Join-Path $outRootPath ($relativeNoExt + ".json")
$outlineSvg = Join-Path $workRoot ($relativeNoExt.Replace("/", "_") + ".outline.svg")
New-Item -ItemType Directory -Force -Path ([System.IO.Path]::GetDirectoryName($outPng)) | Out-Null
New-Item -ItemType Directory -Force -Path ([System.IO.Path]::GetDirectoryName($outlineSvg)) | Out-Null
try {
$viewBox = Read-ViewBox $svg.FullName
if ($viewBox.W -le 0 -or $viewBox.H -le 0) { throw "invalid viewBox" }
$preprocessOutput = & $JavaExe -cp $PreprocessorClasspath combatant.client.render.engine.svg.SvgMsdfPreprocessor $svg.FullName $outlineSvg 2>&1
if ($LASTEXITCODE -ne 0) { throw "svg preprocessor failed: $preprocessOutput" }
if (-not (Test-Path $outlineSvg)) { throw "outline svg was not produced" }
$scale = [Math]::Min($Dimension / $viewBox.W, $Dimension / $viewBox.H)
$translateX = -$viewBox.X
$translateY = -$viewBox.Y
$msdfgenOutput = & $msdfgen $Type -svg $outlineSvg -o $outPng -dimensions $Dimension $Dimension -pxrange $PxRange -scale $scale -translate $translateX $translateY 2>&1
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $outPng)) { throw "msdfgen failed: $msdfgenOutput" }
Write-Metadata $outJson $relative ($relativeNoExt + ".png") $viewBox
$generated++
} catch {
$skipped++
$message = "Skipped ${relative}: $($_.Exception.Message)"
if ($Strict) { throw $message }
Write-Host $message -ForegroundColor Yellow
}
}
Write-Host "Generated $generated SVG MSDF assets, skipped $skipped." -ForegroundColor Green

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
msdfgen.exe -defineshape "{ 1471,0; 1149,0; 1021,333; 435,333; 314,0; 0,0; 571,1466; 884,1466; # }{ 926,580; 724,1124; 526,580; # }" -size 16 16 -autoframe -testrender render.png 1024 1024

Binary file not shown.

BIN
mod/tools/msdf/msdfgen.exe Normal file

Binary file not shown.

View file

@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""Rewrite import paths to match on-disk location + auto-fix missing/star imports.
Usage: fiximports.py [BASE ...] — if no BASE given, scans whole lovisual tree.
Features vs v1:
- handles star imports (import pkg.*) -> expands to single-type imports based on body usage
- inserts missing single-type imports for unique classes used by simple name
- fixes stale FQNs in body (dev.loki.lovisual.oldpkg.Class -> newpkg.Class)
- respects shadow list (Font, Scoreboard etc.) and dupe names
- dedupes + sorts imports
"""
import os, re, sys
ROOT = "src/main/java/dev/loki/lovisual"
# Known shadows where simple name collides with JDK/vanilla/GUI types.
# Auto-insert would break these (seen in ecfe76f fix).
SHADOW = {
"Font",
"GlyphAtlasManager",
"Scoreboard",
"Inventory",
"MediaPlayer",
"Matrix4f",
"Vector3f",
"Button",
"List",
"Map",
"Set",
"Player",
"Entity",
"Level",
"World",
}
# Build class -> pkg map from disk
cls_pkg = {} # cls -> set(pkgs)
cls_pkg_unique = {} # cls -> pkg if unique
file_for_cls = {} # cls -> path (for dupes debug)
scan_roots = sys.argv[1:] if len(sys.argv) > 1 else [ROOT]
for base in scan_roots:
# base may be like src/main/java/dev/loki/lovisual/util/media
# we walk it, but for global map we walk ROOT anyway to fix all imports
pass
# always build from ROOT for correctness
for dp, dns, fns in os.walk(ROOT):
pkg = os.path.relpath(dp, ROOT).replace("/", ".")
if pkg == ".":
pkg = ""
for fn in fns:
if not fn.endswith(".java"):
continue
cls = fn[:-5]
# full pkg for this class
full_pkg = f"dev.loki.lovisual.{pkg}" if pkg else "dev.loki.lovisual"
cls_pkg.setdefault(cls, set()).add(full_pkg)
file_for_cls[cls] = os.path.join(dp, fn)
dupes = {cls for cls, pkgs in cls_pkg.items() if len(pkgs) > 1}
for cls, pkgs in cls_pkg.items():
if len(pkgs) == 1:
cls_pkg_unique[cls] = next(iter(pkgs))
# For FQN fix: build old->new map for stale FQNs (where file moved but old import still points)
# We already have unique map, so any FQN that doesn't match current pkg is stale.
changed = 0
FQN_RE = re.compile(r"dev\.loki\.lovisual\.[\w.]+?\.([A-Z]\w*)\b")
for tree in ("src/main/java", "src/test/java"):
for dp, dns, fns in os.walk(tree):
for fn in fns:
if not fn.endswith(".java"):
continue
p = os.path.join(dp, fn)
text = open(p, encoding="utf-8", errors="ignore").read()
orig = text
# package of this file
m_pkg = re.search(r"^package\s+([\w.]+);", text, re.M)
my_pkg = m_pkg.group(1) if m_pkg else ""
lines = text.splitlines(keepends=True)
# separate header vs body
# find last import line
import_lines = [i for i, l in enumerate(lines) if l.startswith("import ")]
body_start = (
(max(import_lines) + 1)
if import_lines
else next(
(i for i, l in enumerate(lines) if l.startswith("package")), 0
)
+ 1
)
body = "".join(lines[body_start:])
# 1) Fix existing single-type imports where path is stale (unique classes only)
def fix_import(m):
full = m.group(0).strip() # import dev.loki.lovisual.foo.Bar;
# extract pkg and cls
mm = re.match(
r"import\s+(?:static\s+)?dev\.loki\.lovisual\.([\w.]+)\.([A-Z]\w*);",
full,
)
if not mm:
return m.group(0)
pkg_part, cls = mm.group(1), mm.group(2)
full_pkg_prefix = f"dev.loki.lovisual.{pkg_part}"
if cls in dupes or cls in SHADOW:
return m.group(0)
want = cls_pkg_unique.get(cls)
if want is None:
return m.group(0)
if want == full_pkg_prefix or want == f"dev.loki.lovisual.{pkg_part}":
return m.group(0)
# keep static prefix if present
is_static = "static " in full
prefix = "import static " if is_static else "import "
return (
f"{prefix}{want}.{cls};\n"
if full.endswith("\n")
else f"{prefix}{want}.{cls};"
)
text = re.sub(
r"import\s+(?:static\s+)?dev\.loki\.lovisual\.[\w.]*?\.([A-Z]\w*);[ \t]*\n?",
lambda m: fix_import(m) if m.group(1) in cls_pkg_unique else m.group(0),
text,
)
# Re-split after fix
lines = text.splitlines(keepends=True)
import_lines = [i for i, l in enumerate(lines) if l.startswith("import ")]
body_start = (
(max(import_lines) + 1)
if import_lines
else next(
(i for i, l in enumerate(lines) if l.startswith("package")), 0
)
+ 1
)
body = "".join(lines[body_start:])
# 2) Expand star imports: import pkg.* -> needed singles
# DISABLED by default for clean-tree runs (avoids 57-file churn).
# split_struct.py handles star expansion per-wave when BASE becomes empty.
# Enable here only if --expand-stars flag is given.
stars = []
if "--expand-stars" in sys.argv:
for l in lines:
ms = re.match(r"import\s+dev\.loki\.lovisual\.([\w.]+)\.\*;", l)
if ms:
stars.append(f"dev.loki.lovisual.{ms.group(1)}")
if stars:
needed = set()
body_tokens = set(re.findall(r"\b[A-Z][A-Za-z0-9_]*\b", body))
for star_pkg in stars:
for cls in body_tokens:
want = cls_pkg_unique.get(cls)
if want == star_pkg:
if cls not in SHADOW:
needed.add(f"import {want}.{cls};")
if needed:
# remove star lines, insert needed
new_lines = []
for l in lines:
if re.match(r"import\s+dev\.loki\.lovisual\.[\w.]+\.\*;", l):
continue
new_lines.append(l)
# insert needed after last import or after package
lines = new_lines
text = "".join(lines)
lines = text.splitlines(keepends=True)
import_lines = [
i for i, l in enumerate(lines) if l.startswith("import ")
]
idx = (
max(import_lines)
if import_lines
else next(
(i for i, l in enumerate(lines) if l.startswith("package")),
0,
)
)
# dedup against existing
existing = set(l.strip() for l in lines if l.startswith("import "))
to_add = sorted(n for n in needed if n not in existing)
if to_add:
lines[idx + 1 : idx + 1] = [n + "\n" for n in to_add]
text = "".join(lines)
# re-derive body
lines = text.splitlines(keepends=True)
body = "".join(lines[body_start:])
# 3) Insert missing imports for unique classes used by simple name
# DISABLED by default: too aggressive (inner class UiState etc.) — only needed
# when split_struct moved classes and body uses simple name without import.
# split_struct.py already handles this for moved files. For fiximports standalone,
# we skip auto-insert to avoid 160-file churn on clean tree. Enable with --insert.
missing = []
if "--insert" in sys.argv:
body_simple = body
body_simple = re.sub(r'"(\\.|[^"\\])*"', '""', body_simple)
body_simple = re.sub(r"//[^\n]*", "", body_simple)
body_simple = re.sub(r"/\*.*?\*/", "", body_simple, flags=re.S)
tokens = set(re.findall(r"\b[A-Z][A-Za-z0-9_]*\b", body_simple))
for cls in tokens:
if cls in SHADOW or cls in dupes:
continue
want = cls_pkg_unique.get(cls)
if not want:
continue
if want == my_pkg:
continue
if f"import {want}.{cls};" in text:
continue
if cls == fn[:-5]:
continue
if f"{want}.{cls}" in body:
continue
# require unqualified usage (not Outer.Inner)
if not re.search(
r"(?<![\w.])" + re.escape(cls) + r"\b", body_simple
):
continue
missing.append(f"import {want}.{cls};")
if missing:
missing = sorted(set(missing))
lines = text.splitlines(keepends=True)
import_lines = [
i for i, l in enumerate(lines) if l.startswith("import ")
]
idx = (
max(import_lines)
if import_lines
else next(
(i for i, l in enumerate(lines) if l.startswith("package")), 0
)
)
lines[idx + 1 : idx + 1] = [m + "\n" for m in missing]
text = "".join(lines)
# 4) Fix stale FQNs in body (where old pkg.Class still written fully qualified)
def fqn_fix(m):
cls = m.group(1)
full = m.group(0)
if cls in dupes or cls in SHADOW:
return full
want = cls_pkg_unique.get(cls)
if not want:
return full
# full is like dev.loki.lovisual.old.pkg.Cls
cur_pkg = ".".join(full.split(".")[:-1])
if cur_pkg == want:
return full
return f"{want}.{cls}"
# only in body part, but apply to whole file (imports already fixed)
text = FQN_RE.sub(
lambda m: fqn_fix(m) if m.group(1) in cls_pkg_unique else m.group(0),
text,
)
# 5) Dedupe + sort imports block (keep static separate)
lines = text.splitlines(keepends=True)
# find import block range
imp_start = next(
(i for i, l in enumerate(lines) if l.startswith("import ")), None
)
if imp_start is not None:
imp_end = imp_start
while imp_end < len(lines) and lines[imp_end].startswith("import "):
imp_end += 1
block = lines[imp_start:imp_end]
# dedupe preserving order (no global sort to avoid 490-file churn)
seen = set()
uniq = []
for l in block:
if l not in seen:
seen.add(l)
uniq.append(l)
lines = lines[:imp_start] + uniq + lines[imp_end:]
text = "".join(lines)
if text != orig:
open(p, "w", encoding="utf-8").write(text)
changed += 1
print(f"import paths corrected: {changed}")
if dupes:
print(
f"dupes skipped ({len(dupes)}): {sorted(list(dupes))[:10]}{' ...' if len(dupes) > 10 else ''}"
)
if SHADOW:
print(f"shadow protected: {sorted(SHADOW)}")

View file

@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Depth-aware sweep: promote ALL package-private member declarations
(fields, methods, constructors, nested types) inside class bodies of a tree.
Skips private members deliberately? No — the wave policy: everything that was
package-private becomes public; existing public/private untouched.
Usage: member_sweep.py BASE"""
import os, re, sys
PRIMS = {"int","float","double","boolean","long","byte","short","char","void"}
MODS = {"public","private","protected","static","final","abstract","transient","volatile","native","strictfp","synchronized","default"}
TYPE_TOK = re.compile(r"^(?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*(?:<[^>]*>)?(?:\[\])?(?:\?|extends\s+\w+)*$")
def clean(l):
s = re.sub(r"//.*", "", l)
s = re.sub(r'"(\\.|[^"\\])*"', '""', s)
s = re.sub(r"'(\\.|[^'\\])'", "''", s)
return s
def process(p):
lines = open(p).read().splitlines(keepends=True)
stack = []
out = []
changed = 0
for l in lines:
in_class = not stack or stack[-1] == "class"
s = l.strip()
ind = l[:len(l)-len(l.lstrip())]
if in_class and s and ind and l[:4] == " " and not s.startswith(("public","private","protected","//","*","@","}","{","import","package","else","return","if","for","while","switch","case","try","catch","finally","throw","new ","super.","this.","synchronized(")):
m = re.match(r"^(?:(?:static|final|abstract|transient|volatile|synchronized)\s+)*(<[\w ,<>]+>\s+)?([A-Za-z_][\w.]*(?:<[^>]*>)?)(?:\[\])?\s+(\w+)\s*(\(|=|;)", s)
ctor = re.match(r"^(\w+)\s*\(", s) and s.split("(")[0][0].isupper()
typ_ok = False
if m:
typ = m.group(2)
typ_ok = typ in PRIMS or typ[0].isupper()
is_field = m and typ_ok and m.group(4) in ("=",";")
is_method = m and typ_ok and m.group(4) == "(" and not s.startswith("return")
is_ctor = bool(ctor) and ind.strip() != "" and not s.startswith(("class","record","interface","enum","public","static","final","abstract","void","new ")) and re.match(rf"^\w+\(", s) and s.split("(")[0] in [fn for fn in [os.path.basename(p)[:-5]]]
is_type = bool(re.match(r"^(?:(?:static|final|abstract|sealed)\s+)*(class|interface|enum|record)\s+\w+", s))
if is_field or is_method or is_ctor or is_type:
l = ind + "public " + s + ("\n" if l.endswith("\n") else "")
changed += 1
out.append(l)
c = clean(l)
pos = 0
while True:
o = c.find("{", pos); cl = c.find("}", pos)
if o == -1 and cl == -1: break
if cl != -1 and (o == -1 or cl < o):
stack.pop() if stack else None
pos = cl + 1
else:
stack.append("class" if re.search(r"\b(class|interface|enum|record)\s+\w", c[:o]) else "other")
pos = o + 1
if changed:
open(p, "w").write("".join(out))
return changed
def main(base):
total = 0
for dp, dns, fns in os.walk(base):
for fn in fns:
if fn.endswith(".java"):
total += process(os.path.join(dp, fn))
print("member sweep promotions:", total)
if __name__ == "__main__":
main(sys.argv[1])

View file

@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Compile-fix helper (Phase 8.5.1): promote declarations to public until
compileJava+compileTestJava are green. Brace-depth-aware class-body tracking;
statement lines are never touched. Run from project root.
Usage: python3 promote.py BASE_DIR"""
import os, re, subprocess, sys
MODS = {"public","private","protected","static","final","abstract","transient","volatile","native","strictfp"}
PRIMS = {"int","float","double","boolean","long","byte","short","char","void"}
IDENT = re.compile(r"[A-Za-z_][\w.]*")
BASE = "."
def clean_line(l):
s = re.sub(r"//[^\n]*", "", l)
s = re.sub(r'"(\\.|[^"\\])*"', '""', s)
s = re.sub(r"'(\\.|[^'\\])*'", "''", s)
return s
def class_body_flags(p):
"""(line, in_class_body) per line using a stack over '{' scopes."""
lines = open(p).read().splitlines(keepends=True)
stack = []
out = []
for l in lines:
out.append((l, not stack or stack[-1] == "class"))
c = clean_line(l)
pos = 0
while True:
o = c.find("{", pos)
cl = c.find("}", pos)
if o == -1 and cl == -1:
break
if cl != -1 and (o == -1 or cl < o):
if stack:
stack.pop()
pos = cl + 1
else:
head = c[:o]
kind = "class" if re.search(r"\b(class|interface|enum|record|@interface)\s+\w", head) else "other"
stack.append(kind)
pos = o + 1
return out
def build_maps():
typemap, cfiles = {}, {}
for dp, dns, fns in os.walk(BASE):
for fn in fns:
if not fn.endswith(".java"):
continue
p = os.path.join(dp, fn)
cfiles.setdefault(fn[:-5], p)
txt = open(p).read()
for m in re.finditer(r"^ {0,8}(?:public\s+|private\s+|protected\s+|static\s+|final\s+|abstract\s+|sealed\s+)*(class|interface|enum|record)\s+(\w+)", txt, re.M):
typemap.setdefault(m.group(2), p)
return typemap, cfiles
def promote(typemap, cfiles, cls, member):
p = typemap.get(cls) or cfiles.get(cls)
if not p:
return False
for i, (l, in_class) in enumerate(class_body_flags(p)):
if not in_class:
continue
s = l.lstrip()
ind = l[:len(l) - len(s)]
if ind not in (" ", " "):
continue
if s.startswith(("public ", "protected ", "private ", "@", "//", "*", "}", "{")):
continue
if re.match(r"^(?:(?:public|static|final|abstract)\s+)*(class|interface|enum|record)\s+%s\b" % re.escape(member), s):
lines = open(p).read().splitlines(keepends=True)
lines[i] = ind + "public " + s + "\n"
open(p, "w").write("".join(lines))
return True
# constructor of a nested class named `member`
if re.match(r"^%s\s*\(" % re.escape(member), s):
lines = open(p).read().splitlines(keepends=True)
lines[i] = ind + "public " + s + "\n"
open(p, "w").write("".join(lines))
return True
m = re.search(r"(?<![\w.])%s\b\s*(\(|=|;|,)" % re.escape(member), s)
if not m or not s[:m.start()].strip():
continue
toks = s[:m.start()].replace("<", " < ").replace(">", " > ").replace(",", " , ").split()
ok = bool(toks)
for t in toks:
b = IDENT.match(t)
core = b.group(0) if b else ""
if core in MODS or core in PRIMS or (core and core[0].isupper()) or t in ("<", ">", ",", "?", "extends"):
continue
ok = False
break
if not ok:
continue
lines = open(p).read().splitlines(keepends=True)
if s.startswith("private "):
s = s[len("private "):]
lines[i] = ind + "public " + s + ("\n" if l.endswith("\n") else "")
open(p, "w").write("".join(lines))
return True
return False
def main():
global BASE
BASE = sys.argv[1] if len(sys.argv) > 1 else BASE
typemap, cfiles = build_maps()
for it in range(150):
r = subprocess.run(["./gradlew", "compileJava", "compileTestJava", "--offline", "-q"],
capture_output=True, text=True)
err = r.stderr + r.stdout
if r.returncode == 0:
print("COMPILE GREEN after", it, "iterations")
return 0
pend = set()
for msg, c in re.findall(r"error: (.*?) is not public in (\w+);", err):
tok = re.sub(r"<N>", "", msg).split("(")[0].split(".")[-1].strip()
if tok:
pend.add((tok, c))
for msg, c in re.findall(r"reason:\s+(?:<N>)?([\w.]+)\(.*?\) is not public in (\w+);", err):
pend.add((msg.split(".")[-1], c))
for m2, c2 in re.findall(r"method ([\w]+) in class (\w+) cannot be accessed", err):
pend.add((m2, c2))
for m2, c2 in re.findall(r"([A-Z]\w+)\.[\w.]* is defined in an inaccessible class", err):
pass
for cls_name in re.findall(r"([A-Z]\w*) is defined in an inaccessible class or interface", err):
pend.add((cls_name, cls_name))
done = 0
for mem, cls in sorted(pend):
if promote(typemap, cfiles, cls, mem):
done += 1
typemap, cfiles = build_maps()
if done == 0:
print("STUCK at iter", it)
seen, uniq = set(), []
for line in err.splitlines():
if "error:" in line and line not in seen:
seen.add(line)
uniq.append(line)
for u in uniq[:30]:
print(" ", u.replace("/storage/project/jvm/LoVisual/combatant-client-26.2/", "")[:170])
return 1
print(f"iter {it}: promoted {done}/{len(pend)}")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""Structure split helper (Phase 8.5.1): move classes into leaf subpackages by spec JSON,
fix package lines, rewrite FQNs repo-wide, expand star imports of emptied bases,
insert single-type imports for moved classes, promote the PRIMARY top-level type
(class name == file name) to public when it was package-private.
Does NOT run the compile loop — call promote.py afterwards.
Usage: python3 split_struct.py spec.json (run from project root, e.g. combatant-client-26.2)"""
import os, re, subprocess, json, sys
ROOT = "src/main/java/dev/loki/lovisual"
def git_mv(src, dstdir, name):
os.makedirs(dstdir, exist_ok=True)
dst = os.path.join(dstdir, name + ".java")
if os.path.abspath(src) == os.path.abspath(dst):
return
subprocess.run(["git", "mv", "-f", src, dst], check=True)
def main(specpath):
spec = json.load(open(specpath))
BASE = spec["base"]
files = {}
for dp, dns, fns in os.walk(BASE):
for fn in fns:
if fn.endswith(".java"):
files[fn[:-5]] = os.path.join(dp, fn)
reldots = {}
for sub, names in spec["move"].items():
dst = os.path.join(BASE, sub)
os.makedirs(dst, exist_ok=True)
for n in names:
src = files.get(n)
if src is None:
print("MISSING:", n)
continue
cur = os.path.relpath(os.path.dirname(src), ROOT).replace("/", ".")
newp = os.path.relpath(dst, ROOT).replace("/", ".")
if cur == newp:
continue
reldots[f"{cur}.{n}"] = f"{newp}.{n}"
git_mv(src, dst, n)
if not reldots:
print("nothing moved")
# package lines for whole base tree
for dp, dns, fns in os.walk(BASE):
pkg = "dev.loki.lovisual." + os.path.relpath(dp, ROOT).replace("/", ".")
for fn in fns:
if not fn.endswith(".java"):
continue
p = os.path.join(dp, fn)
t = open(p).read()
n = re.sub(r"^package [^;]*;", f"package {pkg};", t, count=1, flags=re.M)
if n != t:
open(p, "w").write(n)
moved_pkg = {}
for dp, dns, fns in os.walk(BASE):
pkg = os.path.relpath(dp, ROOT).replace("/", ".")
for fn in fns:
if fn.endswith(".java"):
moved_pkg[fn[:-5]] = pkg
# global dupes (WorldCommandBuffer, GlyphAtlas* etc.) should not be auto-imported
global_cls = {}
for dp, dns, fns in os.walk(ROOT):
for fn in fns:
if fn.endswith(".java"):
global_cls.setdefault(fn[:-5], set()).add(
os.path.relpath(dp, ROOT).replace("/", ".")
)
dupes = {k for k, v in global_cls.items() if len(v) > 1}
base_pkg = "dev.loki.lovisual." + os.path.relpath(BASE, ROOT).replace("/", ".")
WILD_STAR = f"import {base_pkg}.*;"
WILD_LEGACY = f"import {base_pkg}.;"
SHADOW = {
"Font",
"GlyphAtlasManager",
"Scoreboard",
"Inventory",
"MediaPlayer",
"Player",
"Entity",
"Level",
"World",
}
for tree in ("src/main/java", "src/test/java"):
for dp, dns, fns in os.walk(tree):
my = None
rel = os.path.relpath(dp, tree)
if rel.startswith("dev/loki/lovisual"):
my = rel.replace("/", ".")
for fn in fns:
if not fn.endswith(".java"):
continue
p = os.path.join(dp, fn)
t = open(p).read()
o = t
# replace longest FQNs first to avoid prefix collision (InventorySwap vs InventorySwapVisibility)
for old, new in sorted(reldots.items(), key=lambda kv: -len(kv[0])):
# exact FQN replace: old must be followed by non-word or end, use regex
t = re.sub(
r"dev\.loki\.lovisual\." + re.escape(old) + r"(?=[\W])",
f"dev.loki.lovisual.{new}",
t,
)
# expand star imports (both .* and legacy .%;) only if base became empty
wild_hit = (WILD_STAR in t) or (WILD_LEGACY in t)
if wild_hit:
base_still_has_top = any(
cp == os.path.relpath(BASE, ROOT).replace("/", ".")
for cp in moved_pkg.values()
)
if not base_still_has_top:
body = "\n".join(t.splitlines()[8:])
need = [
f"import dev.loki.lovisual.{cp}.{cls};"
for cls, cp in moved_pkg.items()
if cls not in SHADOW
and cls not in dupes
and re.search(r"(?<![\w.])%s\b" % re.escape(cls), body)
]
repl = "".join(n + "\n" for n in sorted(need))
if WILD_STAR in t:
t = t.replace(WILD_STAR + "\n", repl)
if WILD_LEGACY in t:
t = t.replace(WILD_LEGACY + "\n", repl)
# insert missing single-type imports for moved classes used by simple name
body = "\n".join(t.splitlines()[8:])
need = []
for cls, cp in moved_pkg.items():
if cls in dupes or cls in SHADOW:
continue
if my is not None and cp == my:
continue
if cls == fn[:-5]:
continue
if not re.search(r"(?<![\w.])%s\b" % re.escape(cls), body):
continue
if f"import dev.loki.lovisual.{cp}.{cls};" in t:
continue
need.append(f"import dev.loki.lovisual.{cp}.{cls};")
if need:
lines = t.splitlines(keepends=True)
imps = [i for i, l in enumerate(lines) if l.startswith("import ")]
idx = (
max(imps)
if imps
else next(
(i for i, l in enumerate(lines) if l.startswith("package")),
0,
)
)
lines.insert(idx + 1, "\n".join(sorted(need)) + "\n")
t = "".join(lines)
# promote PRIMARY type only (name == file stem) if package-private
def prom_primary(m):
if m.group(3) != fn[:-5]:
return m.group(0)
return "public " + m.group(0) if not m.group(1) else m.group(0)
t = re.sub(
r"^(public\s+)?((?:(?:final|abstract|sealed|strictfp)\s+)*)(class|interface|enum|record)\s+(\w+)",
lambda m: (
m.group(0)
if (m.group(4) != fn[:-5] or m.group(1))
else "public " + m.group(2) + m.group(3) + " " + m.group(4)
),
t,
count=1,
flags=re.M,
)
if t != o:
open(p, "w").write(t)
print("structure done for", BASE)
if __name__ == "__main__":
main(sys.argv[1])