# mc.muri.io — ATM10 6.6 extras installer # Usage: irm https://mc.muri.io/install.ps1 | iex # Pure-additive: drops the latest extras into your ATM10 mods folder. Never touches ATM10 base mods. $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $baseUrl = 'https://mc.muri.io' $zipUrl = "$baseUrl/mods.zip" function Show-Menu { param( [string[]]$Items, [string]$Title = 'pick a mods folder (up/down, enter to choose, esc to cancel)' ) if ($Items.Count -eq 0) { return $null } $idx = 0 $esc = [char]27 function _renderRow($i, $highlight) { $line = $Items[$i] $maxw = [Math]::Max(40, [Console]::WindowWidth - 4) if ($line.Length -gt $maxw) { $line = $line.Substring(0, $maxw - 1) + '…' } if ($highlight) { Write-Host ("> " + $line) -ForegroundColor Yellow } else { Write-Host (" " + $line) -ForegroundColor Gray } } Write-Host '' Write-Host $Title -ForegroundColor DarkGray for ($i = 0; $i -lt $Items.Count; $i++) { _renderRow $i ($i -eq $idx) } while ($true) { $key = [Console]::ReadKey($true) $changed = $false switch ($key.Key) { 'UpArrow' { if ($idx -gt 0) { $idx--; $changed = $true } } 'DownArrow' { if ($idx -lt $Items.Count - 1) { $idx++; $changed = $true } } 'K' { if ($idx -gt 0) { $idx--; $changed = $true } } 'J' { if ($idx -lt $Items.Count - 1) { $idx++; $changed = $true } } 'Home' { if ($idx -ne 0) { $idx = 0; $changed = $true } } 'End' { if ($idx -ne $Items.Count - 1) { $idx = $Items.Count - 1; $changed = $true } } 'Enter' { Write-Host ''; return $Items[$idx] } 'Escape' { Write-Host ''; return $null } } if ($changed) { # Move cursor up N rows, clear each as we redraw — uses ANSI VT escapes (Win10+, PS5+). [Console]::Write("$esc[$($Items.Count)A") for ($i = 0; $i -lt $Items.Count; $i++) { [Console]::Write("`r$esc[2K") _renderRow $i ($i -eq $idx) } } } } function Find-CandidateModFolders { # Known launcher instance roots (Windows). # Most launchers default to %APPDATA% but several store in %USERPROFILE% or %LOCALAPPDATA% or Documents. # Cases vary (instances / Instances) — Test-Path is case-insensitive on Windows but explicit covers edge cases. $roots = @( # Prism Launcher @{ Path = "$env:APPDATA\PrismLauncher\instances"; Label = 'Prism' }, @{ Path = "$env:APPDATA\PrismLauncher\Instances"; Label = 'Prism' }, # CurseForge App (Overwolf) @{ Path = "$env:USERPROFILE\curseforge\minecraft\Instances"; Label = 'CurseForge' }, @{ Path = "$env:USERPROFILE\curseforge\minecraft\instances"; Label = 'CurseForge' }, @{ Path = "$env:USERPROFILE\Documents\curseforge\minecraft\Instances"; Label = 'CurseForge' }, @{ Path = "$env:USERPROFILE\Documents\Curse\Minecraft\Instances"; Label = 'CurseForge' }, # FTB App (Electron) @{ Path = "$env:LOCALAPPDATA\.ftba\instances"; Label = 'FTB' }, @{ Path = "$env:USERPROFILE\.ftba\instances"; Label = 'FTB' }, @{ Path = "$env:USERPROFILE\Documents\.ftba\instances"; Label = 'FTB' }, # ATLauncher @{ Path = "$env:APPDATA\ATLauncher\instances"; Label = 'ATLauncher' }, @{ Path = "$env:USERPROFILE\ATLauncher\instances"; Label = 'ATLauncher' }, @{ Path = "$env:USERPROFILE\Documents\ATLauncher\instances"; Label = 'ATLauncher' }, # MultiMC (typically portable — these are the common install spots) @{ Path = "$env:APPDATA\MultiMC\instances"; Label = 'MultiMC' }, @{ Path = "$env:USERPROFILE\Documents\MultiMC\instances"; Label = 'MultiMC' }, # PolyMC (deprecated fork of MultiMC, still in use) @{ Path = "$env:APPDATA\PolyMC\instances"; Label = 'PolyMC' }, # Modrinth App (current + legacy Theseus name) @{ Path = "$env:APPDATA\ModrinthApp\profiles"; Label = 'Modrinth' }, @{ Path = "$env:APPDATA\com.modrinth.theseus\profiles"; Label = 'Modrinth' }, # GDLauncher (Next + Carbon) @{ Path = "$env:APPDATA\gdlauncher_next\instances"; Label = 'GDLauncher Next' }, @{ Path = "$env:USERPROFILE\gdlauncher_next\instances"; Label = 'GDLauncher Next' }, @{ Path = "$env:APPDATA\gdlauncher_carbon\instances"; Label = 'GDLauncher' }, @{ Path = "$env:APPDATA\gdlauncher_carbon\data\instances"; Label = 'GDLauncher' }, @{ Path = "$env:USERPROFILE\Documents\GDLauncher\instances"; Label = 'GDLauncher' }, # Technic Launcher @{ Path = "$env:APPDATA\.technic\modpacks"; Label = 'Technic' }, @{ Path = "$env:APPDATA\.technic\instances"; Label = 'Technic' } ) # mods/ may live at one of these subpaths inside an instance: # .minecraft\mods = Prism legacy / FTB / ATLauncher / MultiMC / PolyMC / Technic # minecraft\mods = newer Prism layouts # mods = CurseForge / Modrinth / GDLauncher # instance\.minecraft\mods = GDLauncher Carbon $modsSubpaths = @('mods', 'minecraft\mods', '.minecraft\mods', 'instance\.minecraft\mods', 'instance\mods') $candidates = New-Object System.Collections.Generic.List[pscustomobject] $atmPattern = 'atm.?10|all.?the.?mods' function Get-InstanceMetadata([string]$InstanceDir) { # Reads launcher-specific metadata files to recover the human-readable instance name + version. # FTB stores instances under UUIDs, so the folder name alone won't match ATM10 6.6. $name = $null; $version = $null if (-not (Test-Path $InstanceDir)) { return [pscustomobject]@{ Name = $null; Version = $null } } # JSON metadata: FTB (instance.json with .name + .version), CurseForge (minecraftinstance.json with .installedModpack) $jsonFiles = @('instance.json', 'minecraftinstance.json', 'config.json', 'modpack.json') foreach ($f in $jsonFiles) { $p = Join-Path $InstanceDir $f if (-not (Test-Path $p)) { continue } try { $j = Get-Content $p -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { continue } if (-not $name -and $j.PSObject.Properties['name']) { $name = [string]$j.name } if (-not $version -and $j.PSObject.Properties['version']) { $version = [string]$j.version } # CurseForge: $j.installedModpack if ($j.PSObject.Properties['installedModpack']) { if (-not $name -and $j.installedModpack.name) { $name = [string]$j.installedModpack.name } if (-not $version -and $j.installedModpack.version) { $version = [string]$j.installedModpack.version } } # FTB: $j.modpack if ($j.PSObject.Properties['modpack']) { if (-not $name -and $j.modpack.name) { $name = [string]$j.modpack.name } if (-not $version -and $j.modpack.version) { $version = [string]$j.modpack.version } } # ATLauncher: $j.pack.name + $j.pack.version if ($j.PSObject.Properties['pack']) { if (-not $name -and $j.pack.name) { $name = [string]$j.pack.name } if (-not $version -and $j.pack.version) { $version = [string]$j.pack.version } } } # Prism / MultiMC / PolyMC: instance.cfg (INI). Name = "..." $cfg = Join-Path $InstanceDir 'instance.cfg' if (Test-Path $cfg) { try { $text = Get-Content $cfg -Raw -ErrorAction Stop if (-not $name -and $text -match '(?m)^\s*name\s*=\s*(.+?)\s*$') { $name = $Matches[1] } } catch {} } return [pscustomobject]@{ Name = $name; Version = $version } } function Test-IsAtm66([string]$Path, $Metadata) { # 1) Path component check (works for Prism / CurseForge with named folders like "All-the-Mods-10.6.6") $parts = $Path -split '[\\/]' foreach ($p in $parts) { $low = $p.ToLowerInvariant() if (($low -match '(atm.?10|all.?the.?mods)') -and ($low -match '6\.?6($|[^0-9])')) { return $true } } # 2) Metadata check (works for FTB UUID folders, others with manifests) if ($Metadata) { $combined = (([string]$Metadata.Name + ' ' + [string]$Metadata.Version)).ToLowerInvariant() if (($combined -match '(atm.?10|all.?the.?mods)') -and ($combined -match '6\.?6($|[^0-9])')) { return $true } } return $false } function Add-Candidate($Path, $Launcher, $InstanceFolder, $IsAtm) { $jarCount = (Get-ChildItem $Path -Filter '*.jar' -File -ErrorAction SilentlyContinue | Measure-Object).Count # Walk up from $Path to find the instance directory (skip "mods", ".minecraft", "minecraft", "instance") $instDir = (Get-Item $Path).Parent $skipNames = @('mods', '.minecraft', 'minecraft', 'instance') while ($instDir -and ($skipNames -contains $instDir.Name)) { $instDir = $instDir.Parent } $meta = if ($instDir) { Get-InstanceMetadata $instDir.FullName } else { [pscustomobject]@{ Name = $null; Version = $null } } # Display name preference: metadata name > folder name $displayName = if ($meta.Name) { $meta.Name } else { $InstanceFolder } if ($meta.Version -and $displayName -notlike "*$($meta.Version)*") { $displayName = "$displayName ($($meta.Version))" } $candidates.Add([pscustomobject]@{ Path = $Path Launcher = $Launcher Instance = $displayName IsAtm = ($IsAtm -or ($meta.Name -imatch 'atm.?10|all.?the.?mods')) IsAtm66 = (Test-IsAtm66 $Path $meta) JarCount = $jarCount }) } foreach ($r in $roots) { if (-not (Test-Path $r.Path)) { continue } $instances = Get-ChildItem $r.Path -Directory -ErrorAction SilentlyContinue foreach ($inst in $instances) { foreach ($sub in $modsSubpaths) { $modsPath = Join-Path $inst.FullName $sub if (-not (Test-Path $modsPath)) { continue } Add-Candidate $modsPath $r.Label $inst.Name ($inst.Name -imatch $atmPattern) break # one mods/ per instance is enough } } } # Vanilla / direct .minecraft installs $direct = @( @{ Path = "$env:APPDATA\.minecraft\mods"; Label = 'vanilla'; Instance = '.minecraft' }, @{ Path = "$env:LOCALAPPDATA\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\.minecraft\mods"; Label = 'MS Store'; Instance = '.minecraft' } ) foreach ($d in $direct) { if (Test-Path $d.Path) { Add-Candidate $d.Path $d.Label $d.Instance $false } } # Generic broad scan as last-resort: find any "mods" folder under common Minecraft-launcher roots # that we might have missed (custom install path, portable launcher, etc.). Limited to depth-5 # under a known set of likely roots so this is fast. $genericRoots = @( "$env:USERPROFILE\Documents", "$env:USERPROFILE\Desktop" ) foreach ($gr in $genericRoots) { if (-not (Test-Path $gr)) { continue } Get-ChildItem $gr -Directory -Recurse -Depth 4 -Filter 'mods' -ErrorAction SilentlyContinue | ForEach-Object { # Only count it if it actually has .jar files and isn't already in candidates $alreadyKnown = $candidates | Where-Object { $_.Path -ieq $_.FullName } if (-not $alreadyKnown) { $jars = (Get-ChildItem $_.FullName -Filter '*.jar' -File -ErrorAction SilentlyContinue | Measure-Object).Count if ($jars -gt 0) { # Walk up to find the "instance" name (parent of .minecraft / minecraft / mods) $instDir = $_.Parent if ($instDir.Name -eq '.minecraft' -or $instDir.Name -eq 'minecraft') { $instDir = $instDir.Parent } $launcher = '(generic)' $p = $_.FullName.ToLowerInvariant() if ($p -like '*\prismlauncher\*') { $launcher = 'Prism' } elseif ($p -like '*\curseforge\*') { $launcher = 'CurseForge' } elseif ($p -like '*\multimc\*') { $launcher = 'MultiMC' } elseif ($p -like '*\polymc\*') { $launcher = 'PolyMC' } elseif ($p -like '*\atlauncher\*') { $launcher = 'ATLauncher' } elseif ($p -like '*\.ftba\*') { $launcher = 'FTB' } elseif ($p -like '*\modrinth*') { $launcher = 'Modrinth' } elseif ($p -like '*\gdlauncher*') { $launcher = 'GDLauncher' } elseif ($p -like '*\.technic\*') { $launcher = 'Technic' } Add-Candidate $_.FullName $launcher $instDir.Name ($instDir.Name -imatch $atmPattern) } } } } $unique = @($candidates | Sort-Object -Property Path -Unique | Sort-Object -Property @{Expression='IsAtm';Descending=$true}, @{Expression='JarCount';Descending=$true}, Launcher, Instance) return $unique } function Resolve-ModsDir { param([string]$Hint) if ($Hint -and (Test-Path $Hint)) { return (Resolve-Path $Hint).Path } Write-Host 'searching for Minecraft instances...' -ForegroundColor DarkGray $candidates = Find-CandidateModFolders # Auto-pick if there is exactly one ATM10 6.6 instance — no menu needed $atm66 = @($candidates | Where-Object { $_.IsAtm66 }) if ($atm66.Count -eq 1) { Write-Host ('found ATM10 6.6 instance: {0}' -f $atm66[0].Path) -ForegroundColor Green return $atm66[0].Path } $items = @() foreach ($c in $candidates) { $marker = if ($c.IsAtm66) { '[6.6] ' } elseif ($c.IsAtm) { '[ATM] ' } else { ' ' } $items += ('{0}{1,-15} {2,-40} {3}' -f $marker, $c.Launcher, $c.Instance, $c.Path) } $customLabel = ' [enter custom path...]' $items += $customLabel $choice = Show-Menu -Items $items -Title 'pick the mods folder of your ATM10 instance (up/down, enter)' if (-not $choice) { throw 'cancelled' } if ($choice -eq $customLabel) { $example = Join-Path $env:APPDATA 'PrismLauncher\instances\ATM10\.minecraft\mods' Write-Host '' Write-Host ("example: {0}" -f $example) -ForegroundColor DarkGray $entered = Read-Host 'path to your ATM10 mods folder' if (-not (Test-Path $entered)) { throw "folder not found: $entered" } return (Resolve-Path $entered).Path } # Find the matching candidate by path (last column) $picked = $candidates | Where-Object { $choice.EndsWith($_.Path) } | Select-Object -First 1 if (-not $picked) { throw "internal: cannot resolve picked entry" } return $picked.Path } function Get-JarModIds([string]$JarPath) { # Returns modIds from the jar's [[mods]] blocks only — never from [[dependencies.X]]. $ids = New-Object System.Collections.Generic.HashSet[string] try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue $zip = [System.IO.Compression.ZipFile]::OpenRead($JarPath) try { $names = @('META-INF/neoforge.mods.toml','META-INF/mods.toml','fabric.mod.json') foreach ($n in $names) { $e = $zip.GetEntry($n); if (-not $e) { continue } $sr = New-Object System.IO.StreamReader($e.Open()) try { $content = $sr.ReadToEnd() } finally { $sr.Dispose() } if ($n.EndsWith('.toml')) { $inMods = $false foreach ($line in ($content -split "`r?`n")) { $trim = $line.Trim() if ($trim -match '^\[\[\s*mods\s*\]\]') { $inMods = $true; continue } if ($trim -match '^\[\[') { $inMods = $false; continue } if ($trim -match '^\[(?!\[)') { $inMods = $false; continue } if ($inMods -and ($trim -match '^modId\s*=\s*"([^"]+)"')) { $id = $Matches[1] if ($id -and $id -ne 'neoforge' -and $id -ne 'minecraft' -and $id -ne 'forge' -and $id -ne 'fabricloader') { [void]$ids.Add($id) } } } } else { try { $j = $content | ConvertFrom-Json; if ($j.id) { [void]$ids.Add([string]$j.id) } } catch {} } if ($ids.Count -gt 0) { break } } } finally { $zip.Dispose() } } catch { Write-Host (" warn: cannot read modid from {0} ({1})" -f (Split-Path $JarPath -Leaf), $_.Exception.Message) -ForegroundColor DarkYellow } return ,$ids } function New-BackupDir($Dst) { $bk = Join-Path $Dst ("_mc-muri-backup-" + (Get-Date -Format 'yyyyMMdd-HHmmss')) New-Item -ItemType Directory -Path $bk -Force | Out-Null return $bk } Write-Host '' Write-Host 'mc.muri.io — ATM10 6.6 extras installer' -ForegroundColor Yellow Write-Host '----------------------------------------' $dst = Resolve-ModsDir $args[0] Write-Host ('mods/ : {0}' -f $dst) # 1) Download + extract zip $tmpZip = Join-Path $env:TEMP 'mc-muri-extras.zip' $staging = Join-Path $env:TEMP 'mc-muri-extras' if (Test-Path $staging) { Remove-Item $staging -Recurse -Force } Write-Host ('download: {0}' -f $zipUrl) Invoke-WebRequest $zipUrl -OutFile $tmpZip -UseBasicParsing Expand-Archive -Path $tmpZip -DestinationPath $staging -Force $incoming = Get-ChildItem $staging -Filter '*.jar' -File Write-Host ('incoming: {0} jars' -f $incoming.Count) # 2) Inspect existing jars $existing = @(Get-ChildItem $dst -Filter '*.jar' -File | ForEach-Object { [pscustomobject]@{ Path = $_.FullName; Name = $_.Name; ModIds = (Get-JarModIds $_.FullName) } }) Write-Host ('existing: {0} jars in mods/' -f $existing.Count) # 3) Plan removals: any jar with a modid that an incoming jar also declares $bk = New-BackupDir $dst $removed = @() foreach ($newJar in $incoming) { $newIds = Get-JarModIds $newJar.FullName foreach ($e in $existing) { if (-not (Test-Path $e.Path)) { continue } if ($e.Name -eq $newJar.Name) { continue } $intersect = @($e.ModIds | Where-Object { $newIds.Contains($_) }) if ($intersect.Count -gt 0) { Move-Item $e.Path (Join-Path $bk $e.Name) -Force $removed += $e.Name Write-Host ('- replaced ({0}): {1}' -f ($intersect -join ','), $e.Name) } } } # 4) Copy new jars in foreach ($newJar in $incoming) { Copy-Item $newJar.FullName $dst -Force Write-Host ('+ installed: {0}' -f $newJar.Name) } # 5) Clean up empty backup dir if ((Get-ChildItem $bk -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0) { Remove-Item $bk -Force } Write-Host '' Write-Host ('done. {0} mods installed, {1} replaced.' -f $incoming.Count, $removed.Count) -ForegroundColor Green if ($removed.Count -gt 0) { Write-Host ('backup of replaced jars: {0}' -f $bk) } Write-Host 'launch ATM10 6.6 and connect to mc.muri.io'