commit e2f3f4759d22d0a07df7db476b4a5c07575363b1 Author: simeonvartik Date: Mon Jun 15 06:11:26 2026 +0200 Initial release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5374b6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +* +!/.gitignore +!/README.md +!/LICENSE +!/CREDITS.md +!/build.gradle.kts +!/settings.gradle.kts +!/gradle.properties +!/gradlew +!/gradlew.bat +!/gradle/ +!/gradle/** +!/src/ +!/src/** + +# Build-time icon sources used by generateVanillaUiAssets. +!/assets/ +!/assets/icons/ +!/assets/icons/** + +!/addon-templates/ +!/addon-templates/** +/addon-templates/**/.gradle/ +/addon-templates/**/build/ +/mc-src*/ +/.gradle/ +/build/ +/out/ +/run/ +/src/main/resources/assets/template/ diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..0664d1f --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,41 @@ +# Credits + +AUTISM Client includes original code written for this project, but it also uses, references, or derives ideas/code from other open-source Minecraft client projects. + +Huge credit to the following developers and projects: + +## OpSec by aurickk + +Parts of AUTISM Client related to client privacy, mod detection protection, compatibility handling, and anti-fingerprinting behavior are inspired by or derived from OpSec. + +Project: https://github.com/aurickk/OpSec +License: GPL-3.0 + +## Meteor Client + +AUTISM Client uses ideas, structure, APIs, or source-code references from Meteor Client, especially around Fabric client architecture, modules, utilities, and client-side systems. + +Project: https://github.com/MeteorDevelopment/meteor-client +License: GPL-3.0 + +## Wurst Client + +AUTISM Client uses ideas, references, or code patterns from Wurst Client, one of the long-running open-source Minecraft utility clients. + +Project: https://github.com/Wurst-Imperium/Wurst7 +License: GPL-3.0 + +## ExploitPreventer by NikOverflow + +AUTISM Client uses ideas, references, or derived logic from ExploitPreventer for protection against known client-side exploits. + +Project: https://github.com/NikOverflow/ExploitPreventer +License: MIT + +## License Notice + +Because AUTISM Client uses and/or derives from GPL-licensed projects, AUTISM Client is released under the GNU General Public License v3.0. + +Source code must stay available to users, and any redistributed modified versions must follow the same GPL-3.0 license terms. + +If any credit is missing or inaccurate, contact the maintainers so it can be corrected. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..354f1e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd74ce6 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# AUTISM Client + +AUTISM Client is the best Duping/Debugging client built for people who actually want control over what actions server recieves. + +It is not trying to be a bloated all-in-one mess with 400 random buttons nobody uses. The point is simple: useful modules, a strong macro system, clean addon support, and tools that make testing, automation, and server interaction less painful. + +This client is built around speed, control, and not pretending that users are too stupid to touch advanced features. + +## What It Includes + +AUTISM Client includes the normal client stuff you would expect, but the main focus is on systems that are actually useful. + +Main features include: + +* module system +* macro editor +* custom macro actions +* custom macro conditions +* addon support +* addon presets +* LAN sync tools +* custom payload sender +* NBT viewer +* packet and payload testing tools +* HUD elements +* commands +* event system + +The client is made to be expandable. If the base client is not enough, addons can add their own modules, macro actions, conditions, presets, HUD elements, commands, events, and mixins. + +--- + +# Credits + +AUTISM Client includes original code written for this project, but it also uses, references, or derives ideas/code from other open-source Minecraft client projects. + +Huge credit to the following developers and projects: + +## OpSec by aurickk + +Parts of AUTISM Client related to client privacy, mod detection protection, compatibility handling, and anti-fingerprinting behavior are inspired by or derived from OpSec. + +Project: https://github.com/aurickk/OpSec +License: GPL-3.0 + +## Meteor Client + +AUTISM Client uses ideas, structure, APIs, or source-code references from Meteor Client, especially around Fabric client architecture, modules, utilities, and client-side systems. + +Project: https://github.com/MeteorDevelopment/meteor-client +License: GPL-3.0 + +## Wurst Client + +AUTISM Client uses ideas, references, or code patterns from Wurst Client, one of the long-running open-source Minecraft utility clients. + +Project: https://github.com/Wurst-Imperium/Wurst7 +License: GPL-3.0 + +## ExploitPreventer by NikOverflow + +AUTISM Client uses ideas, references, or derived logic from ExploitPreventer for protection against known client-side exploits. + +Project: https://github.com/NikOverflow/ExploitPreventer +License: MIT + +## Dupe Radar by ErneTalu + +The Radar idea is credited to Dupe Radar by ErneTalu. + +Project: https://github.com/ErneTalu/Dupe-Radar + +## License Notice + +Because AUTISM Client uses and/or derives from GPL-licensed projects, AUTISM Client is released under the GNU General Public License v3.0. + +Source code must stay available to users, and any redistributed modified versions must follow the same GPL-3.0 license terms. + +If any credit is missing or inaccurate, contact the maintainers so it can be corrected. diff --git a/addon-templates/README.md b/addon-templates/README.md new file mode 100644 index 0000000..19f3b18 --- /dev/null +++ b/addon-templates/README.md @@ -0,0 +1,121 @@ +# AUTISM Client Addon Templates + +These are the official addon templates for AUTISM Client. + +Put these in the same repo as the client source. They are standalone Fabric mod projects, but they depend on the AUTISM Client API being published to your local Maven repo first. + +This is not some magical separate SDK. Build the client API locally, then build your addon. + +## Templates + +- `minimal/` - This is the one you should probably start with. + +It gives you the clean basic setup without throwing the entire client architecture at your face. It includes: + +one module +one simple macro action +one simple wait condition +the beginner-friendly API setup + +It is intentionally small. You can actually read it without needing to reverse engineer the entire client. + +- `advanced/` - This is the full reference template. + +Use this when you want to see how the bigger stuff is supposed to be wired. It includes: + +modules +macro actions +macro conditions +presets +commands +HUD elements +events +a mixin + +Addon presets are fully supported. They show up in the macro editor `Presets` picker under your addon's +own category, the same way addon modules/actions/conditions do. + +Category names are intentionally simple: your addon name is the public category. If an API lets you pass a +custom category label, treat that as local organization only. The user-facing UI still groups your stuff under +the addon name so the menu does not turn into a junk drawer. + +Use this when you want to see how everything works together, or when the minimal template stops being enough. + +## Build Flow + +From the main AUTISM Client folder, publish the API locally: + +.\gradlew.bat publishToMavenLocal --no-daemon + +Then build the template you want. + +Minimal: + +cd addon-templates\minimal +.\gradlew.bat build --no-daemon + +Advanced: + +cd addon-templates\advanced +.\gradlew.bat build --no-daemon + +Your addon jar will be in: + +build/libs/ + +That is the file you load as the addon. + +## Toolkit + +There is a toolkit script because renaming addon projects by hand is where people start creating cursed broken projects. + +Run it from the main client folder: + +.\addon-templates\addon-toolkit.ps1 + +Direct commands: + +.\addon-templates\addon-toolkit.ps1 -Action Setup -Template minimal -OutputPath ..\MyAddon +.\addon-templates\addon-toolkit.ps1 -Action Scan -ProjectPath ..\MyAddon +.\addon-templates\addon-toolkit.ps1 -Action BuildAll + +Setup asks for: + +addon name +output folder +version +author + +If you leave the version blank, it bumps the current template version by one. + +It also generates the annoying stuff automatically: + +addon id +package name +Maven group +jar name +description +Fabric entrypoints +mixin references + +This matters because half renaming a mod is one of the easiest ways to make Gradle look like it hates you personally. + +If you want to set every value yourself, use: + +.\addon-templates\addon-toolkit.ps1 -Action Setup -Template advanced -Advanced + +Do that only if you actually need control over everything. Or edit the files directly. + + +## Public Repo Rule + +Commit the source, Gradle wrapper files, and config files. + +Do not commit: + +.gradle/ +build/ + +They are already ignored by the root .gitignore. + +If it is generated trash from your machine, it probably does not belong in the repo. diff --git a/addon-templates/addon-toolkit.ps1 b/addon-templates/addon-toolkit.ps1 new file mode 100644 index 0000000..afa3278 --- /dev/null +++ b/addon-templates/addon-toolkit.ps1 @@ -0,0 +1,908 @@ +param( + [ValidateSet("Menu", "Setup", "Scan", "Validate", "PublishApi", "Build", "BuildAll", "Clean")] + [string] $Action = "Menu", + + [ValidateSet("minimal", "advanced")] + [string] $Template = "minimal", + + [string] $ProjectPath, + [string] $OutputPath, + [string] $AddonId, + [string] $AddonName, + [string] $AddonVersion, + [string] $Package, + [string] $Author, + [string] $Description, + [string] $MavenGroup, + [string] $ArchiveName, + [switch] $Advanced, + [switch] $DebugStack, + [switch] $NoPublish, + [switch] $Yes, + [switch] $NonInteractive +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = Split-Path -Parent $PSScriptRoot + +function Write-Info([string] $Message) { + Write-Host "[AUTISM] $Message" -ForegroundColor DarkGray +} + +function Write-Ok([string] $Message) { + Write-Host "[OK] $Message" -ForegroundColor Green +} + +function Write-Warn([string] $Message) { + Write-Host "[WARN] $Message" -ForegroundColor DarkYellow +} + +function Write-Bad([string] $Message) { + Write-Host "[FAIL] $Message" -ForegroundColor White -BackgroundColor Red +} + +function Write-PathLine([string] $Label, [string] $Path) { + Write-Host "${Label}:" -ForegroundColor DarkGray + Write-Host $Path -ForegroundColor White +} + +function Write-FailureItem([string] $Message) { + if ($Message -match "^[A-Za-z]:[\\/]" -or $Message -match "^/" -or $Message -match "^[.]{1,2}[\\/]") { + Write-PathLine "Path" $Message + } else { + Write-Bad " - $Message" + } +} + +function Resolve-FullPath([string] $Path) { + if ([string]::IsNullOrWhiteSpace($Path)) { + return $null + } + if ([System.IO.Path]::IsPathRooted($Path)) { + return [System.IO.Path]::GetFullPath($Path) + } + return [System.IO.Path]::GetFullPath((Join-Path $RepoRoot $Path)) +} + +function Test-ShippedTemplatePath([string] $Path) { + if ([string]::IsNullOrWhiteSpace($Path)) { + return $false + } + $full = [System.IO.Path]::GetFullPath($Path).TrimEnd("\", "/") + foreach ($templateName in @("minimal", "advanced")) { + $templatePath = [System.IO.Path]::GetFullPath((Join-Path $RepoRoot "addon-templates/$templateName")).TrimEnd("\", "/") + if ([string]::Equals($full, $templatePath, [System.StringComparison]::OrdinalIgnoreCase)) { + return $true + } + } + return $false +} + +function ConvertTo-FolderName([string] $Name) { + if ([string]::IsNullOrWhiteSpace($Name)) { + return "MyAddon" + } + $parts = [regex]::Matches($Name, "[A-Za-z0-9]+") | ForEach-Object { $_.Value } + if (-not $parts -or $parts.Count -eq 0) { + return "MyAddon" + } + $folder = (($parts | ForEach-Object { + if ($_.Length -eq 1) { + $_.ToUpperInvariant() + } else { + $_.Substring(0, 1).ToUpperInvariant() + $_.Substring(1) + } + }) -join "") + if ($folder.Length -gt 48) { + $folder = $folder.Substring(0, 48) + } + return $folder +} + +function ConvertTo-PackageSegment([string] $Value) { + $segment = $Value.ToLowerInvariant() -replace "[^a-z0-9]+", "" + if ([string]::IsNullOrWhiteSpace($segment)) { + $segment = "myaddon" + } + if ($segment[0] -notmatch "[a-z]") { + $segment = "addon$segment" + } + return $segment +} + +function Get-DefaultAuthor { + if (-not [string]::IsNullOrWhiteSpace($env:USERNAME)) { + return $env:USERNAME + } + return "You" +} + +function Get-DefaultAddonOutputPath([string] $TemplateName, [string] $Name) { + $baseName = if ([string]::IsNullOrWhiteSpace($Name)) { + if ($TemplateName -eq "advanced") { "MyAdvancedAddon" } else { "MyAddon" } + } else { + ConvertTo-FolderName $Name + } + return Resolve-FullPath (Join-Path ".." $baseName) +} + +function Read-Value([string] $Prompt, [string] $Default, [scriptblock] $Validator) { + while ($true) { + $suffix = if ([string]::IsNullOrWhiteSpace($Default)) { "" } else { " [$Default]" } + $value = Read-Host "$Prompt$suffix" + if ([string]::IsNullOrWhiteSpace($value)) { + $value = $Default + } + if ($null -eq $Validator -or (& $Validator $value)) { + return $value + } + Write-Warn "Bad value. Fix it." + } +} + +function Read-VersionValue([string] $Current) { + $base = if ([string]::IsNullOrWhiteSpace($Current)) { "1.0.0" } else { $Current } + $next = Step-Version $base + while ($true) { + $value = Read-Host "Version current $base, blank = $next" + if ([string]::IsNullOrWhiteSpace($value)) { + return $next + } + if (Test-Version $value) { + return $value + } + Write-Warn "Bad version. Fix it." + } +} + +function Test-ModId([string] $Value) { + return $Value -match "^[a-z][a-z0-9_-]{1,63}$" +} + +function Test-JavaPackage([string] $Value) { + return $Value -match "^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)+$" +} + +function Test-Version([string] $Value) { + return $Value -match "^[0-9A-Za-z][0-9A-Za-z._+-]*$" +} + +function Step-Version([string] $Version) { + if ([string]::IsNullOrWhiteSpace($Version)) { + return "1.0.0" + } + $match = [regex]::Match($Version, "^(.*?)(\d+)(\D*)$") + if (-not $match.Success) { + return "$Version.1" + } + $prefix = $match.Groups[1].Value + $digits = $match.Groups[2].Value + $suffix = $match.Groups[3].Value + $next = ([int64] $digits) + 1 + $nextText = [string] $next + if ($digits.Length -gt 1 -and $nextText.Length -lt $digits.Length) { + $nextText = $nextText.PadLeft($digits.Length, "0") + } + return "$prefix$nextText$suffix" +} + +function ConvertTo-ModId([string] $Name) { + $id = $Name.ToLowerInvariant() -replace "[^a-z0-9_-]+", "-" + $id = $id.Trim("-_") + if ([string]::IsNullOrWhiteSpace($id)) { + return "my-autism-addon" + } + if ($id[0] -notmatch "[a-z]") { + $id = "addon-$id" + } + if ($id.Length -gt 64) { + $id = $id.Substring(0, 64).Trim("-_") + } + return $id +} + +function Get-TextFile([string] $Path) { + return Get-Content -LiteralPath $Path -Raw +} + +function Set-TextFile([string] $Path, [string] $Text) { + $fullPath = if (Test-Path -LiteralPath $Path) { + (Resolve-Path -LiteralPath $Path).Path + } else { + [System.IO.Path]::GetFullPath($Path) + } + $encoding = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($fullPath, $Text, $encoding) +} + +function Get-JsonFile([string] $Path) { + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json +} + +function Save-JsonFile([string] $Path, $Json) { + $text = $Json | ConvertTo-Json -Depth 64 + $text = $text.Replace("\u003c", "<").Replace("\u003e", ">").Replace("\u0026", "&") + Set-TextFile $Path ($text + [Environment]::NewLine) +} + +function Get-TomlVersion([string] $Path, [string] $Key) { + $text = Get-TextFile $Path + $match = [regex]::Match($text, "(?m)^\s*$([regex]::Escape($Key))\s*=\s*`"([^`"]+)`"") + if ($match.Success) { + return $match.Groups[1].Value + } + return "" +} + +function Set-TomlVersion([string] $Path, [string] $Key, [string] $Value) { + $text = Get-TextFile $Path + $pattern = "(?m)^(\s*$([regex]::Escape($Key))\s*=\s*)`"[^`"]*`"" + if ($text -match $pattern) { + $text = [regex]::Replace($text, $pattern, "`${1}`"$Value`"") + } else { + $text += [Environment]::NewLine + "$Key = `"$Value`"" + [Environment]::NewLine + } + Set-TextFile $Path $text +} + +function Set-PropertiesValue([string] $Path, [string] $Key, [string] $Value) { + $text = Get-TextFile $Path + $pattern = "(?m)^$([regex]::Escape($Key))=.*$" + if ($text -match $pattern) { + $text = [regex]::Replace($text, $pattern, "$Key=$Value") + } else { + $text += [Environment]::NewLine + "$Key=$Value" + [Environment]::NewLine + } + Set-TextFile $Path $text +} + +function Set-RootProjectName([string] $Path, [string] $Name) { + $text = Get-TextFile $Path + $pattern = "(?m)^(rootProject\.name\s*=\s*)`"[^`"]*`"" + if ($text -match $pattern) { + $text = [regex]::Replace($text, $pattern, "`${1}`"$Name`"") + } else { + $text += [Environment]::NewLine + "rootProject.name = `"$Name`"" + [Environment]::NewLine + } + Set-TextFile $Path $text +} + +function Get-FirstJavaPackage([string] $Project) { + $entry = Join-Path $Project "src/main/resources/fabric.mod.json" + if (Test-Path -LiteralPath $entry -PathType Leaf) { + $json = Get-JsonFile $entry + $classes = @() + if ($json.entrypoints.client) { $classes += @($json.entrypoints.client) } + if ($json.entrypoints.autism) { $classes += @($json.entrypoints.autism) } + foreach ($class in $classes) { + if ($class -match "^(.+)\.[^.]+$") { + return $matches[1] + } + } + } + + $java = Get-ChildItem -LiteralPath (Join-Path $Project "src/main/java") -Recurse -Filter *.java -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($java) { + $match = [regex]::Match((Get-TextFile $java.FullName), "(?m)^\s*package\s+([^;]+);") + if ($match.Success) { + return $match.Groups[1].Value.Trim() + } + } + return "com.example.addon" +} + +function Get-ClassSimpleName([string] $ClassName, [string] $Fallback) { + if ($ClassName -match "\.([^.]+)$") { + return $matches[1] + } + return $Fallback +} + +function Replace-InProjectFiles([string] $Project, [string] $Old, [string] $New) { + if ([string]::IsNullOrWhiteSpace($Old) -or $Old -eq $New) { + return + } + $extensions = @(".java", ".json", ".kts", ".toml", ".properties", ".md", ".yml", ".yaml") + $files = Get-ChildItem -LiteralPath $Project -Recurse -File -Force | Where-Object { + $_.FullName -notmatch "\\(build|\.gradle)\\" -and $extensions -contains $_.Extension + } + foreach ($file in $files) { + $text = Get-TextFile $file.FullName + if ($text.Contains($Old)) { + Set-TextFile $file.FullName ($text.Replace($Old, $New)) + } + } +} + +function Move-JavaPackage([string] $Project, [string] $OldPackage, [string] $NewPackage) { + if ($OldPackage -eq $NewPackage) { + return + } + $srcRoot = Join-Path $Project "src/main/java" + $oldPath = Join-Path $srcRoot ($OldPackage.Replace(".", [System.IO.Path]::DirectorySeparatorChar)) + $newPath = Join-Path $srcRoot ($NewPackage.Replace(".", [System.IO.Path]::DirectorySeparatorChar)) + if (-not (Test-Path -LiteralPath $oldPath -PathType Container)) { + return + } + if (Test-Path -LiteralPath $newPath) { + Write-Warn "Package path already exists. Not moving files." + Write-PathLine "Path" $newPath + return + } + + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $newPath) | Out-Null + Move-Item -LiteralPath $oldPath -Destination $newPath + + $cursor = Split-Path -Parent $oldPath + while ($cursor -and $cursor.StartsWith($srcRoot) -and $cursor -ne $srcRoot) { + $children = Get-ChildItem -LiteralPath $cursor -Force + if ($children.Count -gt 0) { + break + } + $next = Split-Path -Parent $cursor + Remove-Item -LiteralPath $cursor + $cursor = $next + } +} + +function Copy-AddonTemplate([string] $TemplateName, [string] $TargetPath) { + $source = Join-Path $RepoRoot "addon-templates/$TemplateName" + if (-not (Test-Path -LiteralPath $source -PathType Container)) { + Write-PathLine "Missing template" $source + throw "Template not found." + } + if (Test-Path -LiteralPath $TargetPath) { + Write-Warn "Output already exists. Editing it in place." + Write-PathLine "Path" $TargetPath + return + } + New-Item -ItemType Directory -Force -Path $TargetPath | Out-Null + $sourceFull = [System.IO.Path]::GetFullPath($source) + $targetFull = [System.IO.Path]::GetFullPath($TargetPath) + Get-ChildItem -LiteralPath $sourceFull -Recurse -Force | Where-Object { + $_.FullName -notmatch "\\(build|\.gradle)($|\\)" + } | ForEach-Object { + $relative = $_.FullName.Substring($sourceFull.Length).TrimStart("\", "/") + $destination = Join-Path $targetFull $relative + if ($_.PSIsContainer) { + New-Item -ItemType Directory -Force -Path $destination | Out-Null + } else { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $destination) | Out-Null + Copy-Item -LiteralPath $_.FullName -Destination $destination -Force + } + } +} + +function Get-AddonProjectPath { + if (-not [string]::IsNullOrWhiteSpace($ProjectPath)) { + return Resolve-FullPath $ProjectPath + } + if (-not [string]::IsNullOrWhiteSpace($OutputPath)) { + $target = Resolve-FullPath $OutputPath + Copy-AddonTemplate $Template $target + return $target + } + return Join-Path $RepoRoot "addon-templates/$Template" +} + +function Get-SetupProjectPath([string] $NameForOutput) { + if (-not [string]::IsNullOrWhiteSpace($ProjectPath)) { + $project = Resolve-FullPath $ProjectPath + if (Test-ShippedTemplatePath $project) { + throw "No. The shipped templates stay clean. Use -OutputPath to copy one, or pick a real addon folder." + } + return $project + } + if ([string]::IsNullOrWhiteSpace($OutputPath)) { + $OutputPath = Read-Value "Output folder" (Get-DefaultAddonOutputPath $Template $NameForOutput) { param($v) -not [string]::IsNullOrWhiteSpace($v) } + } + $target = Resolve-FullPath $OutputPath + if (Test-ShippedTemplatePath $target) { + throw "No. The shipped templates stay clean. Pick a different output folder." + } + Copy-AddonTemplate $Template $target + return $target +} + +function Configure-Addon { + if ([string]::IsNullOrWhiteSpace($AddonName)) { + $defaultName = if ($Template -eq "advanced") { "My Advanced Addon" } else { "My Addon" } + $AddonName = Read-Value "Addon name" $defaultName { param($v) -not [string]::IsNullOrWhiteSpace($v) } + } + + $project = Get-SetupProjectPath $AddonName + if (-not (Test-Path -LiteralPath $project -PathType Container)) { + Write-PathLine "Missing addon folder" $project + throw "Addon project not found." + } + + $fabricPath = Join-Path $project "src/main/resources/fabric.mod.json" + $versionsPath = Join-Path $project "gradle/libs.versions.toml" + $propertiesPath = Join-Path $project "gradle.properties" + $settingsPath = Join-Path $project "settings.gradle.kts" + foreach ($required in @($fabricPath, $versionsPath, $propertiesPath, $settingsPath)) { + if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { + Write-PathLine "Missing file" $required + throw "Missing required addon file." + } + } + + $fabric = Get-JsonFile $fabricPath + $oldId = [string] $fabric.id + $oldName = [string] $fabric.name + $oldPackage = Get-FirstJavaPackage $project + $oldVersion = Get-TomlVersion $versionsPath "mod-version" + $oldClientClass = if ($fabric.entrypoints.client) { [string] @($fabric.entrypoints.client)[0] } else { "$oldPackage.Init" } + $oldAddonClass = if ($fabric.entrypoints.autism) { [string] @($fabric.entrypoints.autism)[0] } else { "$oldPackage.Addon" } + $originalMixinName = if ($fabric.mixins -and @($fabric.mixins).Count -gt 0) { [string] @($fabric.mixins)[0] } else { "" } + $clientSimple = Get-ClassSimpleName $oldClientClass "Init" + $addonSimple = Get-ClassSimpleName $oldAddonClass "Addon" + + if ([string]::IsNullOrWhiteSpace($AddonId)) { + $suggestedId = if ($oldId -match "template") { ConvertTo-ModId $AddonName } else { $oldId } + if ($Advanced) { + $AddonId = Read-Value "Addon id" $suggestedId { param($v) Test-ModId $v } + } else { + $AddonId = $suggestedId + } + } + if ([string]::IsNullOrWhiteSpace($AddonVersion)) { + $suggestedVersion = if ([string]::IsNullOrWhiteSpace($oldVersion)) { "1.0.0" } else { $oldVersion } + $AddonVersion = Read-VersionValue $suggestedVersion + } + if ([string]::IsNullOrWhiteSpace($Package)) { + $suggestedPackage = "com.$(ConvertTo-PackageSegment $AddonId).addon" + if ($Advanced) { + $Package = Read-Value "Java package" $suggestedPackage { param($v) Test-JavaPackage $v } + } else { + $Package = $suggestedPackage + } + } + if ([string]::IsNullOrWhiteSpace($MavenGroup)) { + if ($Advanced) { + $MavenGroup = Read-Value "Maven group" $Package { param($v) Test-JavaPackage $v } + } else { + $MavenGroup = $Package + } + } + if ([string]::IsNullOrWhiteSpace($ArchiveName)) { + if ($Advanced) { + $ArchiveName = Read-Value "Jar/archive base name" $AddonId { param($v) Test-ModId $v } + } else { + $ArchiveName = $AddonId + } + } + if ([string]::IsNullOrWhiteSpace($Author)) { + $Author = Read-Value "Author" (Get-DefaultAuthor) { param($v) -not [string]::IsNullOrWhiteSpace($v) } + } + if ([string]::IsNullOrWhiteSpace($Description)) { + $suggestedDescription = "$AddonName addon for AUTISM Client." + if ($Advanced) { + $Description = Read-Value "Description" $suggestedDescription { param($v) -not [string]::IsNullOrWhiteSpace($v) } + } else { + $Description = $suggestedDescription + } + } + + Replace-InProjectFiles $project $oldPackage $Package + Replace-InProjectFiles $project $oldId $AddonId + Move-JavaPackage $project $oldPackage $Package + + $fabric = Get-JsonFile $fabricPath + $fabric.id = $AddonId + $fabric.name = $AddonName + $fabric.version = '${version}' + $fabric.description = $Description + $fabric.authors = @($Author) + if (-not $fabric.entrypoints) { + $fabric | Add-Member -MemberType NoteProperty -Name entrypoints -Value ([pscustomobject]@{}) + } + $fabric.entrypoints.client = @("$Package.$clientSimple") + $fabric.entrypoints.autism = @("$Package.$addonSimple") + + if ($fabric.mixins -and @($fabric.mixins).Count -gt 0) { + $oldMixinName = if ([string]::IsNullOrWhiteSpace($originalMixinName)) { [string] @($fabric.mixins)[0] } else { $originalMixinName } + $newMixinName = "$AddonId.mixins.json" + $resourceDir = Join-Path $project "src/main/resources" + $oldMixinPath = Join-Path $resourceDir $oldMixinName + $newMixinPath = Join-Path $resourceDir $newMixinName + if ((Test-Path -LiteralPath $oldMixinPath -PathType Leaf) -and $oldMixinPath -ne $newMixinPath) { + Move-Item -LiteralPath $oldMixinPath -Destination $newMixinPath -Force + } + if (Test-Path -LiteralPath $newMixinPath -PathType Leaf) { + $mixinJson = Get-JsonFile $newMixinPath + $mixinJson.package = "$Package.mixin" + Save-JsonFile $newMixinPath $mixinJson + } + $fabric.mixins = @($newMixinName) + } + + Save-JsonFile $fabricPath $fabric + Set-TomlVersion $versionsPath "mod-version" $AddonVersion + Set-PropertiesValue $propertiesPath "maven_group" $MavenGroup + Set-PropertiesValue $propertiesPath "archives_base_name" $ArchiveName + Set-RootProjectName $settingsPath $AddonId + + Write-Ok "Addon rewritten." + Write-PathLine "Addon folder" $project + Write-Host " id: $AddonId" + Write-Host " name: $AddonName" + Write-Host " version: $AddonVersion" + Write-Host " package: $Package" + Write-Host "" + Write-Host "Next:" -ForegroundColor Red + Write-Host ".\addon-templates\addon-toolkit.ps1 -Action Build -ProjectPath `"$project`"" -ForegroundColor White + Write-PathLine "Jar folder" (Join-Path $project "build\libs") + $script:LastConfiguredProject = $project +} + +function Add-Failure([System.Collections.Generic.List[string]] $Failures, [string] $Message) { + $Failures.Add($Message) | Out-Null +} + +function Add-Warning([System.Collections.Generic.List[string]] $Warnings, [string] $Message) { + $Warnings.Add($Message) | Out-Null +} + +function Test-AddonProject([string] $Project, [switch] $AllowTemplateNames) { + $failures = New-Object System.Collections.Generic.List[string] + $warnings = New-Object System.Collections.Generic.List[string] + + if (-not (Test-Path -LiteralPath $Project -PathType Container)) { + Add-Failure $failures "Project folder does not exist." + Add-Failure $failures $Project + return [pscustomobject]@{ Failures = $failures; Warnings = $warnings } + } + + $required = @( + "build.gradle.kts", + "settings.gradle.kts", + "gradle.properties", + "gradle/libs.versions.toml", + "gradlew.bat", + "src/main/resources/fabric.mod.json" + ) + foreach ($item in $required) { + if (-not (Test-Path -LiteralPath (Join-Path $Project $item) -PathType Leaf)) { + Add-Failure $failures "Missing $item" + } + } + + $fabricPath = Join-Path $Project "src/main/resources/fabric.mod.json" + if (Test-Path -LiteralPath $fabricPath -PathType Leaf) { + try { + $fabric = Get-JsonFile $fabricPath + if (-not (Test-ModId ([string] $fabric.id))) { + Add-Failure $failures "fabric.mod.json id is invalid: $($fabric.id)" + } + if ([string]::IsNullOrWhiteSpace([string] $fabric.name)) { + Add-Failure $failures "fabric.mod.json name is empty." + } + if (-not $fabric.entrypoints.autism) { + Add-Failure $failures "Missing autism entrypoint in fabric.mod.json." + } else { + foreach ($entry in @($fabric.entrypoints.autism)) { + $path = Join-Path $Project ("src/main/java/" + ([string] $entry).Replace(".", "/") + ".java") + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + Add-Failure $failures "Autism entrypoint class file is missing: $entry" + } + } + } + if (-not $fabric.depends.autism) { + Add-Failure $failures "fabric.mod.json must depend on autism." + } + if (-not $AllowTemplateNames) { + if ([string] $fabric.id -match "template|example") { + Add-Warning $warnings "Addon id still looks like a template/example." + } + if ([string] $fabric.name -match "Template|Example") { + Add-Warning $warnings "Addon name still looks like a template/example." + } + if (@($fabric.authors) -contains "You") { + Add-Warning $warnings "Author is still 'You'." + } + } + } catch { + Add-Failure $failures "fabric.mod.json is not valid JSON: $($_.Exception.Message)" + } + } + + $versionsPath = Join-Path $Project "gradle/libs.versions.toml" + if (Test-Path -LiteralPath $versionsPath -PathType Leaf) { + $addonVersion = Get-TomlVersion $versionsPath "mod-version" + $hostVersion = Get-TomlVersion $versionsPath "autism" + if (-not (Test-Version $addonVersion)) { + Add-Failure $failures "Addon mod-version is missing or invalid." + } + if ([string]::IsNullOrWhiteSpace($hostVersion)) { + Add-Failure $failures "AUTISM dependency version is missing from gradle/libs.versions.toml." + } + } + + if (Test-Path -LiteralPath (Join-Path $Project ".gradle")) { + Add-Warning $warnings ".gradle cache exists locally. Ignored; do not upload it." + } + if (Test-Path -LiteralPath (Join-Path $Project "build")) { + Add-Warning $warnings "build output exists locally. Ignored; do not upload it." + } + if (Test-Path -LiteralPath (Join-Path $Project ".github")) { + Add-Warning $warnings ".github exists inside this addon project. Fine for a copied template, but it is not root repo CI unless you move it." + } + + $files = Get-ChildItem -LiteralPath $Project -Recurse -File -Force | Where-Object { + $_.FullName -notmatch "\\(build|\.gradle)\\" -and + $_.Extension -in @(".java", ".json", ".md", ".kts", ".toml", ".properties", ".yml", ".yaml") + } + $templateLeftovers = @( + "autism-minimal-addon-template", + "autism-advanced-addon-template", + "AUTISM Minimal Addon Template", + "AUTISM Advanced Addon Template" + ) + foreach ($file in $files) { + $text = Get-TextFile $file.FullName + if ($text -match "[\u00C2\u00C3\u00E2]") { + Add-Warning $warnings "Mojibake-looking text in $($file.FullName)" + } + if (-not $AllowTemplateNames) { + foreach ($leftover in $templateLeftovers) { + if ($text.Contains($leftover)) { + Add-Warning $warnings "Template leftover '$leftover' in $($file.FullName)" + break + } + } + } + } + + return [pscustomobject]@{ Failures = $failures; Warnings = $warnings } +} + +function Scan-Addon { + $project = Get-AddonProjectPath + $result = Test-AddonProject $project + Write-Info "Scanned addon project." + Write-PathLine "Addon folder" $project + foreach ($warning in $result.Warnings) { + Write-Warn "Heads up: $warning" + } + if ($result.Failures.Count -gt 0) { + Write-Bad "Scan found broken stuff:" + foreach ($failure in $result.Failures) { + Write-FailureItem $failure + } + throw "Scan failed." + } + Write-Ok "Scan clean." +} + +function Validate-AddonSystem { + $failures = New-Object System.Collections.Generic.List[string] + $warnings = New-Object System.Collections.Generic.List[string] + + $rootRequired = @( + "src/main/java/autismclient/addons/AddonManager.java", + "src/main/java/autismclient/api/AutismAddon.java", + "src/main/java/autismclient/api/SimpleAddon.java", + "src/main/java/autismclient/api/AutismAddons.java", + "src/main/java/autismclient/api/ApiVersion.java", + "src/main/java/autismclient/api/module/SimpleModule.java", + "src/main/java/autismclient/api/macro/SimpleAction.java", + "src/main/java/autismclient/api/macro/SimpleCondition.java", + "src/main/java/autismclient/api/macro/MacroActionRegistry.java", + "src/main/java/autismclient/api/macro/MacroPresetRegistry.java", + "src/main/java/autismclient/api/hud/HudElements.java", + "src/main/java/autismclient/api/event/AddonEvents.java", + "src/main/java/autismclient/gui/screen/AutismAddonsScreen.java", + "src/main/java/autismclient/util/macro/MissingAddonAction.java", + "addon-templates/README.md", + "addon-templates/addon-toolkit.ps1" + ) + foreach ($file in $rootRequired) { + if (-not (Test-Path -LiteralPath (Join-Path $RepoRoot $file) -PathType Leaf)) { + Add-Failure $failures "Missing $file" + } + } + + $gitignore = Join-Path $RepoRoot ".gitignore" + if (Test-Path -LiteralPath $gitignore -PathType Leaf) { + $ignoreText = Get-TextFile $gitignore + foreach ($pattern in @("!/src/", "!/addon-templates/", "!/addon-templates/**", "/addon-templates/**/build/", "/addon-templates/**/.gradle/", "/mc-src*/")) { + if (-not $ignoreText.Contains($pattern)) { + Add-Failure $failures ".gitignore is missing $pattern" + } + } + } else { + Add-Failure $failures "Missing .gitignore" + } + + $mainVersion = Get-TomlVersion (Join-Path $RepoRoot "gradle/libs.versions.toml") "mod-version" + foreach ($templateName in @("minimal", "advanced")) { + $project = Join-Path $RepoRoot "addon-templates/$templateName" + $result = Test-AddonProject $project -AllowTemplateNames + foreach ($failure in $result.Failures) { + Add-Failure $failures "$templateName template: $failure" + } + foreach ($warning in $result.Warnings) { + Add-Warning $warnings "$templateName template: $warning" + } + $targetVersion = Get-TomlVersion (Join-Path $project "gradle/libs.versions.toml") "autism" + if ($mainVersion -and $targetVersion -and $mainVersion -ne $targetVersion) { + Add-Failure $failures "$templateName template targets AUTISM $targetVersion, main client is $mainVersion." + } + } + + foreach ($warning in $warnings) { + Write-Warn "Heads up: $warning" + } + if ($failures.Count -gt 0) { + Write-Bad "Addon system is broken:" + foreach ($failure in $failures) { + Write-FailureItem $failure + } + throw "Addon system validation failed." + } + Write-Ok "Addon system clean." +} + +function Invoke-Gradle([string] $Project, [string[]] $GradleArgs) { + $gradlew = Join-Path $Project "gradlew.bat" + if (-not (Test-Path -LiteralPath $gradlew -PathType Leaf)) { + Write-PathLine "Missing Gradle wrapper" $gradlew + throw "Missing Gradle wrapper." + } + Push-Location $Project + try { + & $gradlew @GradleArgs + if ($LASTEXITCODE -ne 0) { + Write-PathLine "Gradle project" $Project + throw "Gradle failed with exit code $LASTEXITCODE." + } + } finally { + Pop-Location + } +} + +function Publish-Api { + Write-Info "Publishing client API to Maven Local." + Invoke-Gradle $RepoRoot @("publishToMavenLocal", "--no-daemon") + Write-Ok "Client API published." +} + +function Build-Addon { + $project = Get-AddonProjectPath + if (-not (Test-Path -LiteralPath $project -PathType Container)) { + Write-PathLine "Addon folder" $project + throw "Wrong addon folder." + } + $gradlew = [System.IO.Path]::Combine($project, "gradlew.bat") + if (-not (Test-Path -LiteralPath $gradlew -PathType Leaf)) { + Write-PathLine "Addon folder" $project + throw "No Gradle wrapper there. Wrong addon folder." + } + if (-not $NoPublish) { + Publish-Api + } + Write-Info "Building addon." + Write-PathLine "Addon folder" $project + Invoke-Gradle $project @("build", "--no-daemon") + Write-Ok "Addon built." + Write-PathLine "Addon folder" $project + Write-PathLine "Jar folder" (Join-Path $project "build\libs") +} + +function Build-AllTemplates { + if (-not $NoPublish) { + Publish-Api + } + foreach ($templateName in @("minimal", "advanced")) { + $project = Join-Path $RepoRoot "addon-templates/$templateName" + Write-Info "Building $templateName template." + Invoke-Gradle $project @("build", "--no-daemon") + Write-Ok "$templateName template built." + } +} + +function Clean-Addon { + $project = Get-AddonProjectPath + foreach ($folder in @(".gradle", "build")) { + $path = Join-Path $project $folder + if (Test-Path -LiteralPath $path) { + if ($Yes -or ((Read-Host "Remove $path ? Type YES") -eq "YES")) { + Remove-Item -LiteralPath $path -Recurse -Force + Write-Ok "Deleted local build junk." + Write-PathLine "Path" $path + } + } + } +} + +function Invoke-MenuChoice([scriptblock] $Body) { + try { + & $Body + } catch { + Write-Bad $_.Exception.Message + if ($DebugStack -and $_.ScriptStackTrace) { + Write-Warn $_.ScriptStackTrace + } + [void] (Read-Host "Press Enter. The error stays here until you do") + } +} + +function Show-Menu { + $script:LastAddonPath = "" + while ($true) { + Write-Host "" + Write-Host "AUTISM ADDON KIT" -ForegroundColor Red + Write-Host "1. New addon" + Write-Host "2. Build addon" + Write-Host "3. Scan addon" + Write-Host "4. Test shipped templates" + Write-Host "5. Publish client API" + Write-Host "6. Delete local build junk" + Write-Host "0. Leave" + $choice = Read-Host ">" + switch ($choice) { + "1" { + Invoke-MenuChoice { + $script:Template = Read-Value "Template minimal/advanced" $Template { param($v) $v -in @("minimal", "advanced") } + $script:ProjectPath = "" + $script:OutputPath = "" + Configure-Addon + $script:LastAddonPath = $script:LastConfiguredProject + } + } + "2" { + Invoke-MenuChoice { + $defaultPath = if ([string]::IsNullOrWhiteSpace($script:LastAddonPath)) { Get-DefaultAddonOutputPath $Template "" } else { $script:LastAddonPath } + $script:ProjectPath = Read-Value "Addon folder" $defaultPath { param($v) -not [string]::IsNullOrWhiteSpace($v) } + Build-Addon + } + } + "3" { + Invoke-MenuChoice { + $defaultPath = if ([string]::IsNullOrWhiteSpace($script:LastAddonPath)) { Get-DefaultAddonOutputPath $Template "" } else { $script:LastAddonPath } + $script:ProjectPath = Read-Value "Addon folder" $defaultPath { param($v) -not [string]::IsNullOrWhiteSpace($v) } + Scan-Addon + } + } + "4" { Invoke-MenuChoice { Validate-AddonSystem; Build-AllTemplates } } + "5" { Invoke-MenuChoice { Publish-Api } } + "6" { + Invoke-MenuChoice { + $defaultPath = if ([string]::IsNullOrWhiteSpace($script:LastAddonPath)) { Get-DefaultAddonOutputPath $Template "" } else { $script:LastAddonPath } + $script:ProjectPath = Read-Value "Addon folder" $defaultPath { param($v) -not [string]::IsNullOrWhiteSpace($v) } + Clean-Addon + } + } + "0" { return } + default { Write-Warn "Bad choice." } + } + } +} + +function Invoke-RequestedAction { + switch ($Action) { + "Menu" { Show-Menu } + "Setup" { Configure-Addon } + "Scan" { Scan-Addon } + "Validate" { Validate-AddonSystem } + "PublishApi" { Publish-Api } + "Build" { Build-Addon } + "BuildAll" { Build-AllTemplates } + "Clean" { Clean-Addon } + } +} + +try { + Invoke-RequestedAction +} catch { + Write-Bad $_.Exception.Message + if ($DebugStack -and $_.ScriptStackTrace) { + Write-Warn $_.ScriptStackTrace + } + if ($Action -eq "Menu" -or (-not $NonInteractive -and -not $env:CI)) { + [void] (Read-Host "Press Enter. The error stays here until you do") + } + exit 1 +} diff --git a/addon-templates/advanced/.editorconfig b/addon-templates/advanced/.editorconfig new file mode 100644 index 0000000..e1c9428 --- /dev/null +++ b/addon-templates/advanced/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/addon-templates/advanced/.github/workflows/build.yml b/addon-templates/advanced/.github/workflows/build.yml new file mode 100644 index 0000000..9a609ed --- /dev/null +++ b/addon-templates/advanced/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Build + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 25 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + # NOTE: the build needs the AUTISM Client API artifact (com.autismclient:autism) to be resolvable. + # mavenLocal() is empty on a fresh CI runner, so make it available one of these ways: + # - publish AUTISM to a maven repo your build.gradle.kts lists (GitHub Packages, a maven server), or + # - commit the AUTISM jar into a `libs/` folder and add `flatDir { dirs("libs") }` to repositories. + - name: Build + run: ./gradlew build --stacktrace + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: addon-jar + path: build/libs/*.jar + + - name: Release dev build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v2 + with: + tag_name: dev + name: Dev Build + prerelease: true + files: build/libs/*.jar diff --git a/addon-templates/advanced/.gitignore b/addon-templates/advanced/.gitignore new file mode 100644 index 0000000..d816442 --- /dev/null +++ b/addon-templates/advanced/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +build/ +run/ +*.log diff --git a/addon-templates/advanced/README.md b/addon-templates/advanced/README.md new file mode 100644 index 0000000..39a61b2 --- /dev/null +++ b/addon-templates/advanced/README.md @@ -0,0 +1,93 @@ +# AUTISM Advanced Addon Template + +A standalone reference addon for AUTISM Client. It registers one of each supported extension point, so +you can copy it when you need the full API surface. + +Start small: keep `ExampleModule`, build once, then add macro actions, commands, HUD elements, events, +or mixins as needed. + +## What It Demonstrates + +| Extension point | File | +| --- | --- | +| Addon lifecycle | `ExampleAddon` | +| Module | `modules/ExampleModule` | +| Macro action | `macro/ExampleSayAction` | +| Macro condition | `macro/ExampleHeightCondition` + `macro/ExampleCondition` | +| Macro preset | `macro/ExamplePresets` | +| Command | `commands/ExampleCommand` | +| HUD element | `hud/ExampleHud` | +| Event hooks | `events/ExampleEvents` | +| Mixin | `mixin/ExampleMixin` + `autism-advanced-addon-template.mixins.json` | + +Presets are first-class addon entries. `ExamplePresets` appears in the macro editor `Presets` picker +under this addon's category, and it can insert any normal stack of macro actions. + +Your modules, macro actions, conditions and presets automatically group under a category named after +your addon from `fabric.mod.json`. Custom categories are scoped under your addon so they cannot collide +with built-ins or another addon. + +## Build + +1. In the AUTISM Client project, publish the API to your local Maven repo: + ```sh + ./gradlew publishToMavenLocal + ``` +2. Make sure the `autism` version in `gradle/libs.versions.toml` matches what you published. +3. Build this addon: + ```sh + ./gradlew build + ``` + +The addon jar lands in `build/libs/`. + +## Run + +Drop both jars into your `mods/` folder: + +- the AUTISM Client jar; +- this addon's jar from `build/libs/`. + +## Make It Yours + +Recommended: + +```powershell +..\addon-toolkit.ps1 -Action Setup -Template advanced -OutputPath ..\..\..\MyAdvancedAddon +``` + +The toolkit asks for the addon name, output folder, version, and author. Blank version bumps the current +template version by one patch step. It derives the boring stuff and rewrites Gradle files, package paths, +`fabric.mod.json`, entrypoints, and the mixin config in one shot. + +Manual path: + +1. Rename the package `com.example.addon`. +2. Change `ExampleAddon.ID`. +3. Update `src/main/resources/fabric.mod.json`: `id`, `name`, `entrypoints`, `mixins`, and `autism:color`. +4. Rename `autism-advanced-addon-template.mixins.json` and update its `package`. +5. Keep `apiVersion()` and the `autism` dependency range honest for the client version you target. + +## Project Structure + +```text +addon-templates/advanced/ +|-- gradle/ +|-- src/main/java/com/example/addon/ +| |-- ExampleAddon.java +| |-- ExampleInit.java +| |-- modules/ExampleModule.java +| |-- macro/ExampleSayAction.java +| |-- macro/ExampleHeightCondition.java +| |-- macro/ExampleCondition.java +| |-- macro/ExamplePresets.java +| |-- commands/ExampleCommand.java +| |-- hud/ExampleHud.java +| |-- events/ExampleEvents.java +| `-- mixin/ExampleMixin.java +|-- src/main/resources/ +| |-- fabric.mod.json +| `-- autism-advanced-addon-template.mixins.json +|-- build.gradle.kts +`-- gradle.properties +``` diff --git a/addon-templates/advanced/build.gradle.kts b/addon-templates/advanced/build.gradle.kts new file mode 100644 index 0000000..cd1e0b2 --- /dev/null +++ b/addon-templates/advanced/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.fabric.loom) +} + +base { + archivesName = properties["archives_base_name"] as String + version = libs.versions.mod.version.get() + group = properties["maven_group"] as String +} + +repositories { + // AUTISM Client is consumed from your local Maven repo. In the AUTISM project run: + // ./gradlew publishToMavenLocal + mavenLocal() + maven("https://maven.fabricmc.net/") { name = "Fabric" } + mavenCentral() +} + +dependencies { + // Mirrors the AUTISM Client build: official Mojang mappings are implicit for 26.1.2, so there is no + // explicit `mappings(...)` line and dependencies use plain `implementation`. + minecraft(libs.minecraft) + implementation(libs.fabric.loader) + implementation(libs.fabric.api) + + // The AUTISM Client API (published to mavenLocal from the AUTISM project). + implementation(libs.autism) +} + +// Turns an exact Minecraft version (e.g. "26.1.2") into a patch-compatible range ("~26.1") so the addon +// keeps loading across patch releases instead of pinning one exact build. +fun toMinecraftCompat(version: String): String { + val m = Regex("""^(\d+)\.(\d+)(?:\.(\d+))?$""").matchEntire(version) + ?: return version + val (year, drop, _) = m.destructured + return "~$year.$drop" +} + +tasks { + processResources { + val propertyMap = mapOf( + "version" to project.version, + "mc_version" to libs.versions.minecraft.get(), + "mc_compat" to toMinecraftCompat(libs.versions.minecraft.get()) + ) + inputs.properties(propertyMap) + filteringCharset = "UTF-8" + filesMatching("fabric.mod.json") { + expand(propertyMap) + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } + } + + withType { + options.encoding = "UTF-8" + options.release.set(25) + } +} diff --git a/addon-templates/advanced/gradle.properties b/addon-templates/advanced/gradle.properties new file mode 100644 index 0000000..44ffddf --- /dev/null +++ b/addon-templates/advanced/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=false + +maven_group=com.example.addon +archives_base_name=autism-advanced-addon-template diff --git a/addon-templates/advanced/gradle/libs.versions.toml b/addon-templates/advanced/gradle/libs.versions.toml new file mode 100644 index 0000000..17f6e86 --- /dev/null +++ b/addon-templates/advanced/gradle/libs.versions.toml @@ -0,0 +1,19 @@ +# Mirror these versions with the AUTISM Client release you are building against. +[versions] +mod-version = "1.0.0" + +minecraft = "26.1.2" +fabric-loader = "0.18.4" +fabric-api = "0.147.0+26.1.2" +autism = "2.3" + +loom = "1.16-SNAPSHOT" + +[libraries] +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } +autism = { module = "com.autismclient:autism", version.ref = "autism" } + +[plugins] +fabric-loom = { id = "net.fabricmc.fabric-loom", version.ref = "loom" } diff --git a/addon-templates/advanced/gradle/wrapper/gradle-wrapper.jar b/addon-templates/advanced/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/addon-templates/advanced/gradle/wrapper/gradle-wrapper.jar differ diff --git a/addon-templates/advanced/gradle/wrapper/gradle-wrapper.properties b/addon-templates/advanced/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dbc3ce4 --- /dev/null +++ b/addon-templates/advanced/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/addon-templates/advanced/gradlew b/addon-templates/advanced/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/addon-templates/advanced/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/addon-templates/advanced/gradlew.bat b/addon-templates/advanced/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/addon-templates/advanced/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/addon-templates/advanced/settings.gradle.kts b/addon-templates/advanced/settings.gradle.kts new file mode 100644 index 0000000..e87b8f9 --- /dev/null +++ b/addon-templates/advanced/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven("https://maven.fabricmc.net/") { name = "Fabric" } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "autism-advanced-addon-template" diff --git a/addon-templates/advanced/src/main/java/com/example/addon/ExampleAddon.java b/addon-templates/advanced/src/main/java/com/example/addon/ExampleAddon.java new file mode 100644 index 0000000..f8c7215 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/ExampleAddon.java @@ -0,0 +1,53 @@ +package com.example.addon; + +import autismclient.api.AutismAddon; +import autismclient.api.AutismAddons; +import autismclient.api.ApiVersion; +import autismclient.api.macro.MacroActionEntry; + +import com.example.addon.commands.ExampleCommand; +import com.example.addon.events.ExampleEvents; +import com.example.addon.hud.ExampleHud; +import com.example.addon.macro.ExampleHeightCondition; +import com.example.addon.macro.ExamplePresets; +import com.example.addon.macro.ExampleSayAction; +import com.example.addon.modules.ExampleModule; + +// Addon entrypoint (the "autism" entrypoint in fabric.mod.json). Start small: +// register only ExampleModule first, launch the client, then add macro/actions/HUD/events as needed. +// Modules, actions, conditions and presets all auto-group under a category named after this addon. +public final class ExampleAddon extends AutismAddon { + public static final String ID = "autism-advanced-addon-template"; + + @Override + public int apiVersion() { + return ApiVersion.CURRENT; + } + + @Override + public void onInitialize() { + AutismAddons.modules().register(new ExampleModule()); + + // Action: appears in the "add action" picker. + AutismAddons.macroActions().register(MacroActionEntry.local("say", ExampleSayAction::new) + .picker("Say", "Send a chat message.") + .build()); + + // Condition: appears in the "add condition" picker (it waits). + AutismAddons.macroActions().register(MacroActionEntry.local("wait-height", ExampleHeightCondition::new) + .condition("Wait Height", "Wait until the player is at or above a Y level.") + .build()); + + AutismAddons.presets().register("Reach Height", "Wait for Y 80, then say.", + ExamplePresets::reachHeightThenSay); + + AutismAddons.commands().register(new ExampleCommand()); + AutismAddons.hud().register(new ExampleHud()); + ExampleEvents.register(); + } + + @Override + public String getPackage() { + return "com.example.addon"; + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/ExampleInit.java b/addon-templates/advanced/src/main/java/com/example/addon/ExampleInit.java new file mode 100644 index 0000000..26326ba --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/ExampleInit.java @@ -0,0 +1,9 @@ +package com.example.addon; + +import net.fabricmc.api.ClientModInitializer; + +// Fabric's required client entrypoint. The addon's real work is in ExampleAddon. +public final class ExampleInit implements ClientModInitializer { + @Override + public void onInitializeClient() {} +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/commands/ExampleCommand.java b/addon-templates/advanced/src/main/java/com/example/addon/commands/ExampleCommand.java new file mode 100644 index 0000000..c888baf --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/commands/ExampleCommand.java @@ -0,0 +1,29 @@ +package com.example.addon.commands; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.Command; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; + +// A Brigadier command. Users run it with the command prefix, e.g. .example or .example hi. +public final class ExampleCommand extends Command { + public ExampleCommand() { + super("example", "Example addon command."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("\u00a7dExample addon command works!"); + return SUCCESS; + }); + + root.then(RequiredArgumentBuilder.argument("text", StringArgumentType.greedyString()) + .executes(ctx -> { + AutismClientMessaging.sendPrefixed("\u00a7dYou said: \u00a7f" + StringArgumentType.getString(ctx, "text")); + return SUCCESS; + })); + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/events/ExampleEvents.java b/addon-templates/advanced/src/main/java/com/example/addon/events/ExampleEvents.java new file mode 100644 index 0000000..7901354 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/events/ExampleEvents.java @@ -0,0 +1,15 @@ +package com.example.addon.events; + +import autismclient.api.AutismAddons; +import autismclient.util.AutismClientMessaging; + +// Event hooks. Register listeners once in onInitialize(). +// Available: onTick, onPacketSend (return true to cancel), onPacketReceive, onGameJoin, onGameLeft. +public final class ExampleEvents { + private ExampleEvents() {} + + public static void register() { + AutismAddons.events().onGameJoin(() -> + AutismClientMessaging.sendPrefixed("\u00a7a[Example] joined a world!")); + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/hud/ExampleHud.java b/addon-templates/advanced/src/main/java/com/example/addon/hud/ExampleHud.java new file mode 100644 index 0000000..5b311b0 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/hud/ExampleHud.java @@ -0,0 +1,43 @@ +package com.example.addon.hud; + +import com.example.addon.ExampleAddon; +import autismclient.api.hud.HudElementProvider; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +// A HUD element. Report your size via width()/height(); draw in render() with the GuiGraphicsExtractor +// (ctx.text / ctx.fill). The defaultX/Y/anchor/enabled are used until the user moves it in the HUD editor. +public final class ExampleHud implements HudElementProvider { + private static final int MIN_WIDTH = 94; + private static final int TEXT_PAD = 2; + + @Override public String id() { return ExampleAddon.ID + ":clock"; } + @Override public String label() { return "Example Clock"; } + @Override public String description() { return "Shows the in-game tick."; } + + @Override + public int width() { + Font font = Minecraft.getInstance().font; + String text = text(); + int textWidth = font != null ? font.width(text) : text.length() * 6; + return Math.max(MIN_WIDTH, textWidth + TEXT_PAD * 2); + } + + @Override public int height() { return 10; } + + @Override + public void render(GuiGraphicsExtractor ctx, Font font, int x, int y, float alpha) { + ctx.text(font, text(), x + TEXT_PAD, y, 0xFFFFD7FF); + } + + private static String text() { + long tick = Minecraft.getInstance().level != null ? Minecraft.getInstance().level.getGameTime() : 0L; + return "Addon tick: " + tick; + } + + @Override public boolean defaultEnabled() { return false; } + @Override public String defaultAnchor() { return "TOP_LEFT"; } + @Override public int defaultX() { return 4; } + @Override public int defaultY() { return 120; } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleCondition.java b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleCondition.java new file mode 100644 index 0000000..bc79373 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleCondition.java @@ -0,0 +1,18 @@ +package com.example.addon.macro; + +import autismclient.api.macro.AddonCondition; +import net.minecraft.client.Minecraft; + +// A reusable predicate: implement check(). A waiting step blocks on it via ctx.awaitCondition(...). +public final class ExampleCondition extends AddonCondition { + private final double minY; + + public ExampleCondition(double minY) { + this.minY = minY; + } + + @Override + public boolean check(Minecraft mc) { + return mc.player != null && mc.player.getY() >= minY; + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleHeightCondition.java b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleHeightCondition.java new file mode 100644 index 0000000..0ce20e7 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleHeightCondition.java @@ -0,0 +1,50 @@ +package com.example.addon.macro; + +import com.example.addon.ExampleAddon; +import autismclient.api.macro.ActionSchema; +import autismclient.api.macro.AddonContextAction; +import autismclient.api.macro.MacroExecutionContext; +import net.minecraft.nbt.CompoundTag; + +// A macro condition: a step that waits. Extend AddonContextAction and block in run(), e.g. with +// ctx.awaitCondition(...). Register it with .condition(...) so it lands in the condition picker. +public final class ExampleHeightCondition extends AddonContextAction { + public static final String TYPE_ID = ExampleAddon.ID + ":wait-height"; + + public double minY = 64.0; + + public ExampleHeightCondition() { + super(TYPE_ID); + } + + @Override + public void run(MacroExecutionContext ctx) throws InterruptedException { + ctx.setStatus("Waiting until Y >= " + minY); + ctx.awaitCondition(new ExampleCondition(minY)); + } + + @Override + protected void save(CompoundTag tag) { + putDouble(tag, "minY", minY); + } + + @Override + protected void load(CompoundTag tag) { + minY = getDouble(tag, "minY", minY); + } + + @Override + protected ActionSchema schema() { + return ActionSchema.builder().decimal("minY", "Min Y").decRange(-64, 320).build(); + } + + @Override + public String getDisplayName() { + return "Wait until Y >= " + minY; + } + + @Override + public String getIcon() { + return "Y"; + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/macro/ExamplePresets.java b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExamplePresets.java new file mode 100644 index 0000000..d414126 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExamplePresets.java @@ -0,0 +1,26 @@ +package com.example.addon.macro; + +import autismclient.util.macro.MacroAction; + +import java.util.ArrayList; +import java.util.List; + +// Presets insert a ready-made stack of steps. Return a fresh list each call; don't reuse instances. +public final class ExamplePresets { + private ExamplePresets() {} + + // Wait until the player is high up, then announce it. + public static List reachHeightThenSay() { + List stack = new ArrayList<>(); + + ExampleHeightCondition waitForHeight = new ExampleHeightCondition(); + waitForHeight.minY = 80.0; + stack.add(waitForHeight); + + ExampleSayAction say = new ExampleSayAction(); + say.message = "Reached Y 80!"; + stack.add(say); + + return stack; + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleSayAction.java b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleSayAction.java new file mode 100644 index 0000000..ed096f0 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/macro/ExampleSayAction.java @@ -0,0 +1,50 @@ +package com.example.addon.macro; + +import com.example.addon.ExampleAddon; +import autismclient.api.macro.ActionSchema; +import autismclient.api.macro.AddonAction; +import autismclient.util.AutismClientMessaging; +import net.minecraft.client.Minecraft; +import net.minecraft.nbt.CompoundTag; + +// A macro action: runs once when the step is reached. Extend AddonAction and pass a namespaced id. +public final class ExampleSayAction extends AddonAction { + public static final String TYPE_ID = ExampleAddon.ID + ":say"; + + public String message = "Hello from an addon!"; + + public ExampleSayAction() { + super(TYPE_ID); + } + + @Override + protected void run(Minecraft mc) { + AutismClientMessaging.sendPrefixed("\u00a7d[Example] " + message); + } + + // Field keys here, in load(), and in the schema must match. + @Override + protected void save(CompoundTag tag) { + putString(tag, "message", message); + } + + @Override + protected void load(CompoundTag tag) { + message = getString(tag, "message", message); + } + + @Override + protected ActionSchema schema() { + return ActionSchema.builder().text("message", "Say").build(); + } + + @Override + public String getDisplayName() { + return "Say: " + message; + } + + @Override + public String getIcon() { + return "S"; + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/mixin/ExampleMixin.java b/addon-templates/advanced/src/main/java/com/example/addon/mixin/ExampleMixin.java new file mode 100644 index 0000000..bb0d609 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/mixin/ExampleMixin.java @@ -0,0 +1,19 @@ +package com.example.addon.mixin; + +import com.example.addon.ExampleAddon; +import net.minecraft.client.Minecraft; +import net.minecraft.client.main.GameConfig; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +// Example mixin. List your mixin classes in autism-advanced-addon-template.mixins.json (referenced from +// fabric.mod.json). An @Inject handler matches the target method's params, then CallbackInfo. +@Mixin(Minecraft.class) +public abstract class ExampleMixin { + @Inject(method = "", at = @At("TAIL")) + private void onClientInit(GameConfig gameConfig, CallbackInfo ci) { + org.slf4j.LoggerFactory.getLogger(ExampleAddon.ID).info("Hello from ExampleMixin!"); + } +} diff --git a/addon-templates/advanced/src/main/java/com/example/addon/modules/ExampleModule.java b/addon-templates/advanced/src/main/java/com/example/addon/modules/ExampleModule.java new file mode 100644 index 0000000..cbd19d4 --- /dev/null +++ b/addon-templates/advanced/src/main/java/com/example/addon/modules/ExampleModule.java @@ -0,0 +1,28 @@ +package com.example.addon.modules; + +import com.example.addon.ExampleAddon; +import autismclient.modules.PackModule; +import autismclient.modules.PackModuleOption; +import autismclient.util.AutismClientMessaging; + +// A toggleable module. With no category it auto-lands under a menu column named after your addon. +// Read option values with bool/integer/decimal/choice/text/list(id). Group options with .group("..."). +public final class ExampleModule extends PackModule { + public ExampleModule() { + super(ExampleAddon.ID + ":example", "Example", "Demonstrates an AUTISM addon module."); + + option(PackModuleOption.bool("greet", "Greet on enable", true).group("General")); + option(PackModuleOption.integer("amount", "Amount", 3, 1, 10, 1).group("General")); + option(PackModuleOption.enumChoice("style", "Style", "Friendly", "Friendly", "Loud").group("Display")); + option(PackModuleOption.bool("shout", "Shout in caps", false).group("Display")); + } + + @Override + public void onEnable() { + if (bool("greet")) { + String msg = "[Example] enabled - amount " + integer("amount") + ", style " + choice("style"); + if (bool("shout")) msg = msg.toUpperCase(java.util.Locale.ROOT); + AutismClientMessaging.sendPrefixed("\u00a7b" + msg); + } + } +} diff --git a/addon-templates/advanced/src/main/resources/autism-advanced-addon-template.mixins.json b/addon-templates/advanced/src/main/resources/autism-advanced-addon-template.mixins.json new file mode 100644 index 0000000..0ff885a --- /dev/null +++ b/addon-templates/advanced/src/main/resources/autism-advanced-addon-template.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "com.example.addon.mixin", + "compatibilityLevel": "JAVA_25", + "client": [ + "ExampleMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/addon-templates/advanced/src/main/resources/fabric.mod.json b/addon-templates/advanced/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..db00b11 --- /dev/null +++ b/addon-templates/advanced/src/main/resources/fabric.mod.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 1, + "id": "autism-advanced-addon-template", + "version": "${version}", + "name": "AUTISM Advanced", + "description": "Reference addon for AUTISM Client: a module, macro action, preset, condition, command, HUD element, and event hook.", + "authors": [ + "You" + ], + "custom": { + "autism:color": "255,80,80", + "modmenu": { + "parent": { + "id": "autism" + } + } + }, + "environment": "client", + "entrypoints": { + "client": [ + "com.example.addon.ExampleInit" + ], + "autism": [ + "com.example.addon.ExampleAddon" + ] + }, + "mixins": [ + "autism-advanced-addon-template.mixins.json" + ], + "depends": { + "java": ">=25", + "minecraft": "${mc_compat}", + "fabricloader": ">=0.18.4", + "fabric-api": "*", + "autism": ">=2.3" + } +} diff --git a/addon-templates/minimal/.editorconfig b/addon-templates/minimal/.editorconfig new file mode 100644 index 0000000..e1c9428 --- /dev/null +++ b/addon-templates/minimal/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/addon-templates/minimal/.gitignore b/addon-templates/minimal/.gitignore new file mode 100644 index 0000000..d816442 --- /dev/null +++ b/addon-templates/minimal/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +build/ +run/ +*.log diff --git a/addon-templates/minimal/README.md b/addon-templates/minimal/README.md new file mode 100644 index 0000000..9b0be32 --- /dev/null +++ b/addon-templates/minimal/README.md @@ -0,0 +1,42 @@ +# AUTISM Minimal Addon Template + +Smallest useful AUTISM addon example: + +- one module; +- one macro action; +- one macro condition. + +This template uses the easy API (`SimpleAddon`, `SimpleModule`, `simpleAction`, `simpleCondition`). +Use `../advanced` when you need commands, HUD elements, events, raw custom macro serialization, or mixins. + +## Build + +1. From the AUTISM Client project, publish the API locally: + ```powershell + .\gradlew.bat publishToMavenLocal --no-daemon + ``` +2. From this folder, build the addon: + ```powershell + .\gradlew.bat build --no-daemon + ``` + +The jar lands in `build/libs/`. + +## Customize + +Recommended: + +```powershell +..\addon-toolkit.ps1 -Action Setup -Template minimal -OutputPath ..\..\..\MyAddon +``` + +The toolkit asks for the addon name, output folder, version, and author. Blank version bumps the current +template version by one patch step. It derives the boring stuff and rewrites Gradle files, package paths, +`fabric.mod.json`, and entrypoints in one shot. + +Manual path: + +1. Rename package `com.example.minimal`. +2. Change `MinimalAddon.ID`. +3. Update `src/main/resources/fabric.mod.json`. +4. Change names/descriptions in `MinimalAddon`. diff --git a/addon-templates/minimal/build.gradle.kts b/addon-templates/minimal/build.gradle.kts new file mode 100644 index 0000000..cd1e0b2 --- /dev/null +++ b/addon-templates/minimal/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.fabric.loom) +} + +base { + archivesName = properties["archives_base_name"] as String + version = libs.versions.mod.version.get() + group = properties["maven_group"] as String +} + +repositories { + // AUTISM Client is consumed from your local Maven repo. In the AUTISM project run: + // ./gradlew publishToMavenLocal + mavenLocal() + maven("https://maven.fabricmc.net/") { name = "Fabric" } + mavenCentral() +} + +dependencies { + // Mirrors the AUTISM Client build: official Mojang mappings are implicit for 26.1.2, so there is no + // explicit `mappings(...)` line and dependencies use plain `implementation`. + minecraft(libs.minecraft) + implementation(libs.fabric.loader) + implementation(libs.fabric.api) + + // The AUTISM Client API (published to mavenLocal from the AUTISM project). + implementation(libs.autism) +} + +// Turns an exact Minecraft version (e.g. "26.1.2") into a patch-compatible range ("~26.1") so the addon +// keeps loading across patch releases instead of pinning one exact build. +fun toMinecraftCompat(version: String): String { + val m = Regex("""^(\d+)\.(\d+)(?:\.(\d+))?$""").matchEntire(version) + ?: return version + val (year, drop, _) = m.destructured + return "~$year.$drop" +} + +tasks { + processResources { + val propertyMap = mapOf( + "version" to project.version, + "mc_version" to libs.versions.minecraft.get(), + "mc_compat" to toMinecraftCompat(libs.versions.minecraft.get()) + ) + inputs.properties(propertyMap) + filteringCharset = "UTF-8" + filesMatching("fabric.mod.json") { + expand(propertyMap) + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } + } + + withType { + options.encoding = "UTF-8" + options.release.set(25) + } +} diff --git a/addon-templates/minimal/gradle.properties b/addon-templates/minimal/gradle.properties new file mode 100644 index 0000000..147ec1c --- /dev/null +++ b/addon-templates/minimal/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=false + +maven_group=com.example.addon +archives_base_name=autism-minimal-addon-template diff --git a/addon-templates/minimal/gradle/libs.versions.toml b/addon-templates/minimal/gradle/libs.versions.toml new file mode 100644 index 0000000..17f6e86 --- /dev/null +++ b/addon-templates/minimal/gradle/libs.versions.toml @@ -0,0 +1,19 @@ +# Mirror these versions with the AUTISM Client release you are building against. +[versions] +mod-version = "1.0.0" + +minecraft = "26.1.2" +fabric-loader = "0.18.4" +fabric-api = "0.147.0+26.1.2" +autism = "2.3" + +loom = "1.16-SNAPSHOT" + +[libraries] +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } +autism = { module = "com.autismclient:autism", version.ref = "autism" } + +[plugins] +fabric-loom = { id = "net.fabricmc.fabric-loom", version.ref = "loom" } diff --git a/addon-templates/minimal/gradle/wrapper/gradle-wrapper.jar b/addon-templates/minimal/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/addon-templates/minimal/gradle/wrapper/gradle-wrapper.jar differ diff --git a/addon-templates/minimal/gradle/wrapper/gradle-wrapper.properties b/addon-templates/minimal/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dbc3ce4 --- /dev/null +++ b/addon-templates/minimal/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/addon-templates/minimal/gradlew b/addon-templates/minimal/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/addon-templates/minimal/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/addon-templates/minimal/gradlew.bat b/addon-templates/minimal/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/addon-templates/minimal/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/addon-templates/minimal/settings.gradle.kts b/addon-templates/minimal/settings.gradle.kts new file mode 100644 index 0000000..21cb2c5 --- /dev/null +++ b/addon-templates/minimal/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven("https://maven.fabricmc.net/") { name = "Fabric" } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "autism-minimal-addon-template" diff --git a/addon-templates/minimal/src/main/java/com/example/minimal/MinimalAddon.java b/addon-templates/minimal/src/main/java/com/example/minimal/MinimalAddon.java new file mode 100644 index 0000000..dc6b30e --- /dev/null +++ b/addon-templates/minimal/src/main/java/com/example/minimal/MinimalAddon.java @@ -0,0 +1,35 @@ +package com.example.minimal; + +import autismclient.api.ApiVersion; +import autismclient.api.SimpleAddon; +import autismclient.api.macro.MacroActionEntry; +import autismclient.api.module.SimpleModule; +import autismclient.util.AutismClientMessaging; + +public final class MinimalAddon extends SimpleAddon { + public static final String ID = "autism-minimal-addon-template"; + + public MinimalAddon() { + super(ApiVersion.CURRENT, "com.example.minimal"); + } + + @Override + protected void initialize() { + SimpleModule module = new SimpleModule("hello-module", "Hello Module", "Tiny example addon module.") + .addBool("greet", "Greet On Enable", true) + .addText("message", "Message", "Hello from my addon") + .onEnabled(self -> { + if (self.getBool("greet")) { + AutismClientMessaging.sendPrefixed("\u00a7a" + self.getText("message")); + } + }); + registerModule(module); + + registerAction(simpleAction("say-hello", "Say Hello", "Send a short addon message.", "S", mc -> + AutismClientMessaging.sendPrefixed("\u00a7aHello from the minimal addon!"))); + + MacroActionEntry condition = simpleCondition("wait-player", "Wait Player", "Wait until the local player exists.", + "Waiting for player", "P", mc -> mc.player != null); + registerAction(condition); + } +} diff --git a/addon-templates/minimal/src/main/java/com/example/minimal/MinimalInit.java b/addon-templates/minimal/src/main/java/com/example/minimal/MinimalInit.java new file mode 100644 index 0000000..6a512ed --- /dev/null +++ b/addon-templates/minimal/src/main/java/com/example/minimal/MinimalInit.java @@ -0,0 +1,8 @@ +package com.example.minimal; + +import net.fabricmc.api.ClientModInitializer; + +public final class MinimalInit implements ClientModInitializer { + @Override + public void onInitializeClient() {} +} diff --git a/addon-templates/minimal/src/main/resources/fabric.mod.json b/addon-templates/minimal/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1488334 --- /dev/null +++ b/addon-templates/minimal/src/main/resources/fabric.mod.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": 1, + "id": "autism-minimal-addon-template", + "version": "${version}", + "name": "AUTISM Minimal Addon Template", + "description": "Small AUTISM Client addon example: one module, one macro action, and one macro condition.", + "authors": [ + "You" + ], + "custom": { + "autism:color": "80,220,120", + "modmenu": { + "parent": { + "id": "autism" + } + } + }, + "environment": "client", + "entrypoints": { + "client": [ + "com.example.minimal.MinimalInit" + ], + "autism": [ + "com.example.minimal.MinimalAddon" + ] + }, + "depends": { + "java": ">=25", + "minecraft": "${mc_compat}", + "fabricloader": ">=0.18.4", + "fabric-api": "*", + "autism": ">=2.3" + } +} diff --git a/assets/icons/ChatCategory.svg b/assets/icons/ChatCategory.svg new file mode 100644 index 0000000..1668ce1 --- /dev/null +++ b/assets/icons/ChatCategory.svg @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Fabricator.svg b/assets/icons/Fabricator.svg new file mode 100644 index 0000000..09a5488 --- /dev/null +++ b/assets/icons/Fabricator.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Filter.svg b/assets/icons/Filter.svg new file mode 100644 index 0000000..fdd653b --- /dev/null +++ b/assets/icons/Filter.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Keybinds.svg b/assets/icons/Keybinds.svg new file mode 100644 index 0000000..9831a47 --- /dev/null +++ b/assets/icons/Keybinds.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Lansync.svg b/assets/icons/Lansync.svg new file mode 100644 index 0000000..fcec3f9 --- /dev/null +++ b/assets/icons/Lansync.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Macros.svg b/assets/icons/Macros.svg new file mode 100644 index 0000000..fecf319 --- /dev/null +++ b/assets/icons/Macros.svg @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icons/MainMenuCategory.svg b/assets/icons/MainMenuCategory.svg new file mode 100644 index 0000000..d577140 --- /dev/null +++ b/assets/icons/MainMenuCategory.svg @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/PacketCategory.svg b/assets/icons/PacketCategory.svg new file mode 100644 index 0000000..d337416 --- /dev/null +++ b/assets/icons/PacketCategory.svg @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icons/PacketQEditor.svg b/assets/icons/PacketQEditor.svg new file mode 100644 index 0000000..930c3a4 --- /dev/null +++ b/assets/icons/PacketQEditor.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Packetlogger.svg b/assets/icons/Packetlogger.svg new file mode 100644 index 0000000..90fa46e --- /dev/null +++ b/assets/icons/Packetlogger.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/ScreenCategory.svg b/assets/icons/ScreenCategory.svg new file mode 100644 index 0000000..b9e5bbc --- /dev/null +++ b/assets/icons/ScreenCategory.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/Serverinfo.svg b/assets/icons/Serverinfo.svg new file mode 100644 index 0000000..c166006 --- /dev/null +++ b/assets/icons/Serverinfo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..9728a1e --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,542 @@ +import java.io.OutputStream +import java.util.zip.ZipFile + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.apache.xmlgraphics:batik-transcoder:1.17") + classpath("org.apache.xmlgraphics:batik-codec:1.17") + } +} + +plugins { + alias(libs.plugins.fabric.loom) + `maven-publish` +} + +base { + archivesName = properties["archives_base_name"] as String + version = libs.versions.mod.version.get() + group = properties["maven_group"] as String +} + +repositories { + maven { + name = "meteor-maven" + url = uri("https://maven.meteordev.org/releases") + } + mavenCentral() +} + +dependencies { + + minecraft(libs.minecraft) + implementation(libs.fabric.loader) + implementation(libs.fabric.api) + + implementation("net.java.dev.jna:jna:5.13.0") + implementation("net.java.dev.jna:jna-platform:5.13.0") + implementation("io.netty:netty-handler-proxy:4.1.118.Final") + implementation("io.netty:netty-codec-socks:4.1.118.Final") + include("io.netty:netty-handler-proxy:4.1.118.Final") + include("io.netty:netty-codec-socks:4.1.118.Final") + implementation("de.florianreuth:waybackauthlib:1.1.0") + include("de.florianreuth:waybackauthlib:1.1.0") + implementation("com.github.weisj:jsvg:2.1.0") + include("com.github.weisj:jsvg:2.1.0") + + // Bundle a newer MixinExtras (jar-in-jar) so Fabric Loader loads it over the old 0.5.0 it ships + // with. Required because ViaFabricPlus (and our own WrapOperation/ModifyExpressionValue mixins) + // need MixinExtras >= 0.5.3; without this, joining a server crashes during mixin application. + implementation("io.github.llamalad7:mixinextras-fabric:0.5.4") + include("io.github.llamalad7:mixinextras-fabric:0.5.4") + +} + +val generatedAutismResourcesDir = layout.buildDirectory.dir("generated/resources/autism/main") + +data class SourceFile(val path: String, val text: String) +data class FieldSpec(val name: String, val type: String, val kind: String, val editable: Boolean) +data class PacketSpec( + val className: String, + val protocol: String, + val direction: String, + val codecStyle: String, + val packetType: String, + val source: String, + val complete: Boolean, + val fields: List +) + +sourceSets { + main { + resources.srcDir(generatedAutismResourcesDir) + } +} + +val generateVanillaUiAssets by tasks.registering { + // Semantic feature icons used by the vanilla-friendly UI. Structural + // actions such as close and reorder are rendered as text symbols. + val iconSourceDir = file("assets/icons") + val outputDir = generatedAutismResourcesDir.map { it.dir("assets/autismclient") } + + inputs.dir(iconSourceDir) + outputs.dir(outputDir) + + doLast { + val targetRoot = outputDir.get().asFile + targetRoot.parentFile.resolve("yu" + "ng" + "light").deleteRecursively() + targetRoot.deleteRecursively() + val iconTargetDir = targetRoot.resolve("textures/gui/vanillaui/icons") + + iconTargetDir.mkdirs() + + val pngTranscoderClass = Class.forName("org.apache.batik.transcoder.image.PNGTranscoder") + val transcoderInputClass = Class.forName("org.apache.batik.transcoder.TranscoderInput") + val transcoderOutputClass = Class.forName("org.apache.batik.transcoder.TranscoderOutput") + val transcodingHintsKeyClass = Class.forName("org.apache.batik.transcoder.TranscodingHints\$Key") + + val transcoder = pngTranscoderClass.getDeclaredConstructor().newInstance() + val widthField = pngTranscoderClass.getField("KEY_WIDTH") + val heightField = pngTranscoderClass.getField("KEY_HEIGHT") + val addHint = pngTranscoderClass.getMethod("addTranscodingHint", transcodingHintsKeyClass, Any::class.java) + addHint.invoke(transcoder, widthField.get(null), 256f) + addHint.invoke(transcoder, heightField.get(null), 256f) + + val inputCtor = transcoderInputClass.getConstructor(String::class.java) + val outputCtor = transcoderOutputClass.getConstructor(OutputStream::class.java) + val transcode = pngTranscoderClass.getMethod("transcode", transcoderInputClass, transcoderOutputClass) + + iconSourceDir.listFiles { file -> file.isFile && file.extension.equals("svg", ignoreCase = true) } + ?.sortedBy { it.name.lowercase() } + ?.forEach { svg -> + val outputFile = iconTargetDir.resolve(svg.nameWithoutExtension.lowercase() + ".png") + outputFile.outputStream().use { out -> + val input = inputCtor.newInstance(svg.toURI().toString()) + val output = outputCtor.newInstance(out) + transcode.invoke(transcoder, input, output) + } + } + } +} + +val generateAutismInspectorMappings by tasks.registering { + val mappingFiles = fileTree(".gradle/loom-cache/source_mappings") { + include("**/*.tiny") + } + val outputFile = generatedAutismResourcesDir.map { it.file("autism-inspector-mappings.tsv") } + + inputs.files(mappingFiles) + outputs.file(outputFile) + + doLast { + run { + val target = outputFile.get().asFile + target.parentFile.mkdirs() + target.writeText( + "# Official Mojang mappings are used for Minecraft 26.1.2; no Yarn aliases are generated.\n", + Charsets.UTF_8 + ) + return@doLast + } + + val tinyFile = mappingFiles.files + .filter { it.isFile } + .maxByOrNull { it.lastModified() } + ?: error("Missing Loom source mappings under ${project.file(".gradle/loom-cache/source_mappings").absolutePath}") + + val lines = tinyFile.readLines(Charsets.UTF_8) + val header = lines.firstOrNull { it.startsWith("tiny\t") } + ?: error("Invalid tiny mapping file: ${tinyFile.absolutePath}") + val namespaces = header.split('\t').drop(3) + val namedIndex = namespaces.indexOf("named") + val intermediaryIndex = namespaces.indexOf("intermediary") + require(namedIndex >= 0 && intermediaryIndex >= 0) { + "Tiny mapping header must expose named and intermediary namespaces: $header" + } + + fun readNamespace(parts: List, baseOffset: Int, namespaceIndex: Int): String { + val index = baseOffset + namespaceIndex + return if (index in parts.indices) parts[index].trim() else "" + } + + val namedToIntermediary = linkedMapOf() + val classAliases = linkedMapOf() + + for (raw in lines) { + val trimmed = raw.trimStart('\t') + if (!trimmed.startsWith("c\t")) continue + val parts = trimmed.split('\t') + val namedName = readNamespace(parts, 1, namedIndex) + val intermediaryName = readNamespace(parts, 1, intermediaryIndex) + if (namedName.isBlank() || intermediaryName.isBlank()) continue + namedToIntermediary[namedName] = intermediaryName + classAliases[intermediaryName.replace('/', '.')] = namedName.substringAfterLast('/').replace('$', '.') + } + + fun remapDescriptorToIntermediary(descriptor: String): String { + if (descriptor.isBlank()) return descriptor + val classRef = Regex("L([^;]+);") + return classRef.replace(descriptor) { match -> + val namedInternalName = match.groupValues[1] + val intermediaryInternalName = namedToIntermediary[namedInternalName] ?: namedInternalName + "L$intermediaryInternalName;" + } + } + + fun storeAlias( + target: MutableMap, + ambiguous: MutableSet, + key: String, + alias: String + ) { + if (key.isBlank() || alias.isBlank() || ambiguous.contains(key)) return + val existing = target[key] + if (existing == null) { + target[key] = alias + return + } + if (existing != alias) { + target.remove(key) + ambiguous.add(key) + } + } + + val fieldAliases = linkedMapOf() + val methodAliases = linkedMapOf() + val ambiguousFieldKeys = linkedSetOf() + val ambiguousMethodKeys = linkedSetOf() + + var currentOwner = "" + for (raw in lines) { + val trimmed = raw.trimStart('\t') + if (trimmed.isBlank()) continue + val parts = trimmed.split('\t') + when (parts.firstOrNull()) { + "c" -> { + currentOwner = readNamespace(parts, 1, intermediaryIndex).replace('/', '.') + } + + "f" -> { + if (currentOwner.isBlank()) continue + val descriptor = if (parts.size > 1) remapDescriptorToIntermediary(parts[1]) else "" + val namedName = readNamespace(parts, 2, namedIndex) + val intermediaryName = readNamespace(parts, 2, intermediaryIndex) + if (namedName.isBlank() || intermediaryName.isBlank()) continue + storeAlias(fieldAliases, ambiguousFieldKeys, "$currentOwner#$intermediaryName#$descriptor", namedName) + } + + "m" -> { + if (currentOwner.isBlank()) continue + val descriptor = if (parts.size > 1) remapDescriptorToIntermediary(parts[1]) else "" + val namedName = readNamespace(parts, 2, namedIndex) + val intermediaryName = readNamespace(parts, 2, intermediaryIndex) + if (namedName.isBlank() || intermediaryName.isBlank()) continue + if (namedName == "" || namedName == "") continue + storeAlias(methodAliases, ambiguousMethodKeys, "$currentOwner#$intermediaryName#$descriptor", namedName) + } + } + } + + val authAliasFragments = listOf( + "session", "accesstoken", "refreshtoken", "authtoken", + "clientsession", "playersession", "publicsession" + ) + fun isAuthAlias(alias: String): Boolean { + val normalized = alias.lowercase().replace(Regex("[^a-z0-9]"), "") + return authAliasFragments.any { normalized.contains(it) } + } + + val filteredClassAliases = classAliases.filter { (_, alias) -> !isAuthAlias(alias) } + val blockedOwners = classAliases.keys.filter { owner -> + val alias = classAliases[owner] ?: return@filter false + isAuthAlias(alias) + }.toSet() + + val target = outputFile.get().asFile + target.parentFile.mkdirs() + target.printWriter(Charsets.UTF_8).use { out -> + out.println("# Yarn deobfuscation mappings") + filteredClassAliases.toSortedMap().forEach { (owner, alias) -> + out.println("C\t$owner\t$alias") + } + fieldAliases.toSortedMap().forEach { (key, alias) -> + val parts = key.split('#', limit = 3) + if (parts.size == 3 && !blockedOwners.contains(parts[0]) && !isAuthAlias(alias)) { + out.println("F\t${parts[0]}\t${parts[1]}\t${parts[2]}\t$alias") + } + } + methodAliases.toSortedMap().forEach { (key, alias) -> + val parts = key.split('#', limit = 3) + if (parts.size == 3 && !blockedOwners.contains(parts[0]) && !isAuthAlias(alias)) { + out.println("M\t${parts[0]}\t${parts[1]}\t${parts[2]}\t$alias") + } + } + } + } +} + +val generateAutismPacketSchemas by tasks.registering { + val minecraftVersion = libs.versions.minecraft.get() + val outputFile = generatedAutismResourcesDir.map { it.file("autism-packet-schemas.tsv") } + + outputs.file(outputFile) + + doLast { + fun normalizeWhitespace(value: String): String = value.replace(Regex("\\s+"), " ").trim() + + fun splitTopLevelComma(value: String): List { + val out = mutableListOf() + var depth = 0 + var start = 0 + for (i in value.indices) { + when (value[i]) { + '<', '(', '[', '{' -> depth++ + '>', ')', ']', '}' -> if (depth > 0) depth-- + ',' -> if (depth == 0) { + out += value.substring(start, i).trim() + start = i + 1 + } + } + } + val tail = value.substring(start).trim() + if (tail.isNotBlank()) out += tail + return out + } + + fun protocolFromPath(path: String): String { + val normalized = path.replace('\\', '/') + val marker = "/network/protocol/" + val idx = normalized.indexOf(marker) + val tail = if (idx >= 0) normalized.substring(idx + marker.length) else normalized.substringAfter("net/minecraft/network/protocol/", "") + return tail.substringBefore('/').ifBlank { "unknown" } + } + + fun directionFromName(simpleName: String): String = when { + simpleName.startsWith("Clientbound") -> "S2C" + simpleName.startsWith("Serverbound") -> "C2S" + else -> "ANY" + } + + fun kindForType(type: String): String { + val lower = type.lowercase() + return when { + lower == "byte" || lower == "short" || lower == "int" || lower == "long" || lower == "float" || lower == "double" -> "number" + lower == "boolean" -> "boolean" + lower == "string" -> "string" + lower.contains("itemstack") || lower.contains("hashedstack") -> "item" + lower.contains("component") -> "component" + lower.contains("identifier") || lower.contains("resourcekey") -> "identifier" + lower.contains("holder<") || lower == "holder" -> "holder" + lower.contains("blockpos") || lower.contains("chunkpos") -> "position" + lower.contains("vec3") || lower.contains("positionmoverotation") -> "vector" + lower.contains("uuid") -> "uuid" + lower.startsWith("optional<") || lower.contains(".optional<") -> "optional" + lower.startsWith("list<") || lower.startsWith("set<") || lower.contains("list<") || lower.contains("set<") -> "list" + lower.startsWith("map<") || lower.contains("map<") || lower.contains("int2objectmap") -> "map" + lower.contains("bitset") -> "bitset" + lower.contains("enumset") || lower.contains("relative") || lower.contains("containerinput") -> "enum" + else -> "object" + } + } + + fun editableFor(kind: String): Boolean = kind in setOf("number", "boolean", "string", "identifier", "enum", "uuid") + + fun parseComponent(raw: String): FieldSpec? { + val cleaned = normalizeWhitespace(raw) + .replace(Regex("^@[\\w.]+(?:\\([^)]*\\))?\\s+"), "") + val idx = cleaned.lastIndexOf(' ') + if (idx <= 0 || idx >= cleaned.length - 1) return null + val type = cleaned.substring(0, idx).trim() + val name = cleaned.substring(idx + 1).trim().removeSuffix("...") + if (!name.matches(Regex("[A-Za-z_$][A-Za-z0-9_$]*"))) return null + val kind = kindForType(type) + return FieldSpec(name, type, kind, editableFor(kind)) + } + + fun parseFields(text: String): Pair> { + val record = Regex("""public\s+record\s+\w+\s*\((.*?)\)\s*implements""", setOf(RegexOption.DOT_MATCHES_ALL)) + .find(text) + if (record != null) { + val fields = splitTopLevelComma(record.groupValues[1]).mapNotNull(::parseComponent) + return "record" to fields + } + + val fields = Regex("""(?m)^\s*(?:private|protected)\s+final\s+([^;=]+?)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*;""") + .findAll(text) + .mapNotNull { match -> + val type = normalizeWhitespace(match.groupValues[1]) + val name = match.groupValues[2] + val kind = kindForType(type) + FieldSpec(name, type, kind, editableFor(kind)) + } + .toList() + return if (fields.isNotEmpty()) "fields" to fields else "fallback" to emptyList() + } + + fun loadSources(): List { + val sourcesJar = file("${System.getProperty("user.home")}/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/$minecraftVersion/minecraft-merged-deobf-$minecraftVersion-sources.jar") + if (sourcesJar.isFile) { + ZipFile(sourcesJar).use { zip -> + return zip.entries().asSequence() + .filter { !it.isDirectory } + .filter { it.name.startsWith("net/minecraft/network/protocol/") && it.name.endsWith("Packet.java") } + .map { entry -> + SourceFile(entry.name, zip.getInputStream(entry).bufferedReader(Charsets.UTF_8).use { it.readText() }) + } + .toList() + } + } + + val localRoot = file("mc-src$minecraftVersion/libs/net.minecraft.minecraft-merged-deobf/net/minecraft/network/protocol") + if (localRoot.isDirectory) { + return localRoot.walkTopDown() + .filter { it.isFile && it.name.endsWith("Packet.java") } + .map { file -> + SourceFile(file.relativeTo(localRoot.parentFile.parentFile.parentFile.parentFile).invariantSeparatorsPath, file.readText(Charsets.UTF_8)) + } + .toList() + } + + error("Missing Minecraft deobf sources jar and local mc-src fallback for $minecraftVersion") + } + + fun packetTypeOf(text: String): String { + return Regex("""return\s+([A-Za-z0-9_]+PacketTypes\.[A-Z0-9_]+)\s*;""") + .find(text) + ?.groupValues + ?.get(1) + ?: "" + } + + fun codecStyleOf(text: String): String = when { + text.contains("StreamCodec.unit") -> "UNIT" + text.contains("StreamCodec.composite") -> "COMPOSITE" + text.contains("Packet.codec") -> "PACKET_CODEC" + else -> "CUSTOM" + } + + fun escape(value: String): String = value + .replace('\t', ' ') + .replace('\n', ' ') + .replace('|', '/') + .replace('~', '-') + + val specs = loadSources().mapNotNull { source -> + val pkg = Regex("""package\s+([A-Za-z0-9_.]+)\s*;""").find(source.text)?.groupValues?.get(1) ?: return@mapNotNull null + val simpleName = Regex("""public\s+(?:abstract\s+)?(?:record|class)\s+([A-Za-z0-9_]+Packet)\b""") + .find(source.text) + ?.groupValues + ?.get(1) + ?: return@mapNotNull null + val codecStyle = codecStyleOf(source.text) + val parsed = parseFields(source.text) + val sourceKind = if (codecStyle == "UNIT") "unit" else parsed.first + val complete = codecStyle == "UNIT" || parsed.second.isNotEmpty() + PacketSpec( + "$pkg.$simpleName", + protocolFromPath(source.path), + directionFromName(simpleName), + codecStyle, + packetTypeOf(source.text), + sourceKind, + complete, + if (codecStyle == "UNIT") emptyList() else parsed.second + ) + }.sortedBy { it.className } + + val target = outputFile.get().asFile + target.parentFile.mkdirs() + target.printWriter(Charsets.UTF_8).use { out -> + out.println("# Generated from Minecraft $minecraftVersion packet sources. Fields preserve source/record order where available.") + out.println("# class\tprotocol\tdirection\tcodec\tpacketType\tsource\tcomplete\tfields(name~type~kind~editable|...)") + for (spec in specs) { + val fields = spec.fields.joinToString("|") { field -> + listOf(field.name, field.type, field.kind, field.editable.toString()).joinToString("~", transform = ::escape) + } + val columns = listOf( + spec.className, + spec.protocol, + spec.direction, + spec.codecStyle, + spec.packetType, + spec.source, + spec.complete.toString() + ).joinToString("\t", transform = ::escape) + out.println("$columns\t$fields") + } + } + } +} + +tasks { + processResources { + dependsOn(generateAutismInspectorMappings) + dependsOn(generateAutismPacketSchemas) + dependsOn(generateVanillaUiAssets) + val propertyMap = mapOf( + "version" to project.version, + "mc_version" to libs.versions.minecraft.get() + ) + + inputs.properties(propertyMap) + + filteringCharset = "UTF-8" + + exclude("addon-template.mixins.json") + exclude("assets/template/**") + + filesMatching("fabric.mod.json") { + expand(propertyMap) + } + } + + jar { + inputs.property("archivesName", project.base.archivesName.get()) + + from("LICENSE") { + rename { "${it}_${inputs.properties["archivesName"]}" } + } + + // ModMenu API is a compile-only soft dependency: we ship local stubs of its two API + // interfaces so we can compile the integration without a cross-version ModMenu artifact, but + // we must NOT bundle them — at runtime the real ModMenu provides them (and if ModMenu is + // absent, our integration entrypoint is simply never loaded). + exclude("com/terraformersmc/**") + } + + java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } + } + + withType { + options.encoding = "UTF-8" + options.release.set(25) + options.compilerArgs.add("-Xlint:deprecation") + options.compilerArgs.add("-Xlint:unchecked") + options.compilerArgs.add("-Xlint:-restricted") + } +} + +// Publish the Loom-remapped jar to the local Maven repo so the standalone addon-template (and any +// third-party addon) can depend on it via `modImplementation("com.autismclient:autism:")`. +// Run: ./gradlew publishToMavenLocal +publishing { + publications { + create("mavenJava") { + groupId = "com.autismclient" + artifactId = "autism" + version = project.version.toString() + from(components["java"]) + } + } + repositories { + mavenLocal() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f7d7443 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.configuration-cache=false + +maven_group=com.autismclient +archives_base_name=AUTISM Client diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d34a40d --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +mod-version = "2.5" + +minecraft = "26.1.2" +fabric-loader = "0.18.4" +fabric-api = "0.147.0+26.1.2" + +loom = "1.16-SNAPSHOT" + +[libraries] + +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } + +[plugins] +fabric-loom = { id = "net.fabricmc.fabric-loom", version.ref = "loom" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dbc3ce4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8937131 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = "Fabric" + url = uri("https://maven.fabricmc.net/") + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "YungLightUI" diff --git a/src/main/java/autismclient/AutismClientAddon.java b/src/main/java/autismclient/AutismClientAddon.java new file mode 100644 index 0000000..5cafbb4 --- /dev/null +++ b/src/main/java/autismclient/AutismClientAddon.java @@ -0,0 +1,17 @@ +package autismclient; + +import com.mojang.logging.LogUtils; +import net.fabricmc.loader.api.FabricLoader; +import org.slf4j.Logger; + +public final class AutismClientAddon { + public static final Logger LOG = LogUtils.getLogger(); + public static final String MOD_ID = "autism"; + public static final java.io.File FOLDER = FabricLoader.getInstance().getConfigDir().resolve("autism").toFile(); + + static { + FOLDER.mkdirs(); + } + + private AutismClientAddon() {} +} diff --git a/src/main/java/autismclient/AutismClientMod.java b/src/main/java/autismclient/AutismClientMod.java new file mode 100644 index 0000000..15d12aa --- /dev/null +++ b/src/main/java/autismclient/AutismClientMod.java @@ -0,0 +1,102 @@ +package autismclient; + +import autismclient.gui.vanillaui.components.UiText; +import autismclient.modules.PackAutoReconnectState; +import autismclient.modules.AutismModule; +import autismclient.security.AutismProtector; +import autismclient.security.AutismProtectorPackStrip; +import autismclient.security.AutismProtectorServerPackFailureGuard; +import autismclient.security.AutismProtectorTracker; +import autismclient.security.AutismProtectorVanillaKeys; +import autismclient.util.AutismInstaBreakRenderer; +import autismclient.util.AutismFakeGamemode; +import autismclient.util.AutismJoinMacroController; +import autismclient.util.AutismLANSync; +import autismclient.util.AutismOverlayManager; +import autismclient.util.AutismSvgHudLogo; +import autismclient.util.AutismWindowBranding; +import autismclient.util.AutismSharedState; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.server.packs.PackType; + +public final class AutismClientMod implements ClientModInitializer { + @Override + @SuppressWarnings("deprecation") + public void onInitializeClient() { + AutismClientAddon.FOLDER.mkdirs(); + + AutismModule.get().initialize(); + + AutismProtectorTracker.bootstrap(); + + if (!AutismProtector.isOverlapExternalProtectorPresent()) { + AutismProtectorVanillaKeys.primeAsync(); + } + if (AutismProtector.isFullExternalProtectorPresent()) { + AutismClientAddon.LOG.info( + "[AutismProtector] External protection mod detected; deferring all anti-fingerprint mixins to it."); + } else if (AutismProtector.isExploitPreventerPresent()) { + AutismClientAddon.LOG.info( + "[AutismProtector] ExploitPreventer detected; deferring overlapping protections while keeping brand/channel hiding active."); + } else { + AutismClientAddon.LOG.info( + "[AutismProtector] Built-in anti-fingerprint layer active."); + } + AutismInstaBreakRenderer.initialize(); + autismclient.commands.AutismCommands.init(); + + autismclient.addons.AddonManager.init(); + + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener() { + @Override + public net.minecraft.resources.Identifier getFabricId() { + return net.minecraft.resources.Identifier.fromNamespaceAndPath("autismclient", "ui_assets"); + } + + @Override + public void onResourceManagerReload(net.minecraft.server.packs.resources.ResourceManager manager) { + UiText.onClientResourceReload(); + AutismSvgHudLogo.clear(); + } + }); + + ClientTickEvents.END_CLIENT_TICK.register(client -> { + AutismInstaBreakRenderer.tickPlacePreview(); + AutismWindowBranding.apply(client); + AutismModule.get().tick(); + AutismJoinMacroController.onClientTick(client); + }); + ClientTickEvents.END_LEVEL_TICK.register(level -> AutismLANSync.getInstance().onLevelTick(level.getGameTime())); + ClientConfigurationConnectionEvents.INIT.register((listener, client) -> AutismModule.get().onConfigurationConnectionStarted()); + ClientConfigurationConnectionEvents.DISCONNECT.register((listener, client) -> { + AutismFakeGamemode.clear(); + AutismInstaBreakRenderer.clear(); + AutismProtectorServerPackFailureGuard.clear(); + AutismOverlayManager.get().hideAllInteractiveOverlays(); + AutismModule.get().onGameLeft(); + }); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { + PackAutoReconnectState.remember(client.getCurrentServer()); + AutismModule.get().onGameJoin(); + AutismJoinMacroController.onPlayJoin(); + autismclient.addons.AddonManager.surfaceFailuresOnJoin(); + }); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + AutismFakeGamemode.clear(); + AutismInstaBreakRenderer.clear(); + AutismProtectorPackStrip.clearAll(); + AutismProtectorServerPackFailureGuard.clear(); + AutismOverlayManager.get().hideAllInteractiveOverlays(); + AutismJoinMacroController.onGameLeft(); + AutismModule.get().onGameLeft(); + }); + ItemTooltipCallback.EVENT.register((stack, tooltipContext, tooltipType, lines) -> + AutismModule.get().appendTooltip(stack, lines) + ); + } +} diff --git a/src/main/java/autismclient/addons/AddonManager.java b/src/main/java/autismclient/addons/AddonManager.java new file mode 100644 index 0000000..3a22dad --- /dev/null +++ b/src/main/java/autismclient/addons/AddonManager.java @@ -0,0 +1,507 @@ +package autismclient.addons; + +import autismclient.AutismClientAddon; +import autismclient.api.ApiVersion; +import autismclient.api.AutismAddon; +import autismclient.util.AutismConfig; +import autismclient.util.AutismNotifications; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.entrypoint.EntrypointContainer; +import net.fabricmc.loader.api.metadata.CustomValue; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.fabricmc.loader.api.metadata.Person; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +public final class AddonManager { + private static final int MAX_UI_ADDON_NAME_LENGTH = 22; + private static final List LOADED = new ArrayList<>(); + private static final Map REPORTS = new LinkedHashMap<>(); + + private static String currentAddonId = null; + + private static String currentAddonName = null; + + private static boolean failuresSurfaced = false; + + private AddonManager() {} + + public static void init() { + LOADED.clear(); + REPORTS.clear(); + failuresSurfaced = false; + + AutismConfig config = AutismConfig.getGlobal(); + List accepted = new ArrayList<>(); + for (EntrypointContainer container : FabricLoader.getInstance() + .getEntrypointContainers("autism", AutismAddon.class)) { + ModContainer mod; + try { + mod = container.getProvider(); + } catch (Throwable t) { + AutismClientAddon.LOG.error("[Addons] Failed to inspect an addon entrypoint", t); + continue; + } + + String modId = mod.getMetadata().getId(); + AddonReport report = ensureReport(mod.getMetadata()); + if (isDisabledOnRestart(modId)) { + report.markDisabled("Disabled in AUTISM config. Enable it and restart to load."); + continue; + } + + AutismAddon addon; + try { + addon = container.getEntrypoint(); + } catch (Throwable t) { + report.markFailed("construct", "Failed to construct entrypoint: " + shortError(t)); + AutismClientAddon.LOG.error("[Addons] Failed to construct addon '{}'", modId, t); + continue; + } + + try { + int declared = addon.apiVersion(); + report.setApiVersion(declared); + if (declared <= 0) { + report.markFailed("apiVersion", "Invalid API version " + declared + "."); + AutismClientAddon.LOG.warn("[Addons] Addon '{}' declared invalid API v{}; skipping it.", modId, declared); + continue; + } + if (declared > ApiVersion.CURRENT) { + report.markNeedsNewerApi("Needs API v" + declared + ", host provides v" + ApiVersion.CURRENT + "."); + AutismClientAddon.LOG.warn("[Addons] Addon '{}' needs API v{} but this client provides v{}; skipping it.", + modId, declared, ApiVersion.CURRENT); + continue; + } + if (declared < ApiVersion.CURRENT) { + AutismClientAddon.LOG.info("[Addons] Addon '{}' built against API v{} (current v{}); loading.", + modId, declared, ApiVersion.CURRENT); + } + applyMetadata(addon, mod.getMetadata()); + report.copyRuntimeMetadata(addon); + accepted.add(new LoadedAddon(modId, addon)); + } catch (AbstractMethodError e) { + report.markMissingApiVersion("Missing apiVersion() override."); + AutismClientAddon.LOG.error("[Addons] Addon '{}' does not declare apiVersion(); skipping it.", modId, e); + } catch (Throwable t) { + report.markFailed("prepare", "Failed to prepare: " + shortError(t)); + AutismClientAddon.LOG.error("[Addons] Error preparing addon '{}'", modId, t); + } + } + + List categoryReady = new ArrayList<>(); + for (LoadedAddon la : accepted) { + if (runPhase(la, "onRegisterCategories", la.addon::onRegisterCategories)) { + categoryReady.add(la); + } else { + rollbackRegistrations(la.modId); + } + } + + for (LoadedAddon la : categoryReady) { + if (runPhase(la, "onInitialize", la.addon::onInitialize)) { + LOADED.add(la.addon); + AddonReport report = REPORTS.get(la.modId); + if (report != null) report.markLoaded(); + } else { + rollbackRegistrations(la.modId); + } + } + + if (!LOADED.isEmpty()) { + AutismClientAddon.LOG.info("[Addons] Loaded {} addon(s).", LOADED.size()); + } + if (config.disabledAddonIds != null) { + for (String id : config.disabledAddonIds) { + if (id == null || id.isBlank() || REPORTS.containsKey(id)) continue; + AddonReport report = new AddonReport(id.trim(), id.trim(), "", "", 0xFFAA66FF); + report.markDisabled("Disabled in AUTISM config, but no matching addon entrypoint was discovered."); + REPORTS.put(report.modId(), report); + } + } + } + + private static boolean runPhase(LoadedAddon la, String phase, Runnable body) { + currentAddonId = la.modId; + currentAddonName = la.addon.name == null || la.addon.name.isBlank() ? la.modId : la.addon.name; + AddonReport report = REPORTS.get(la.modId); + if (report != null) report.setPhase(phase); + try { + body.run(); + return true; + } catch (Throwable t) { + if (report != null) report.markFailed(phase, "Threw during " + phase + ": " + shortError(t)); + AutismClientAddon.LOG.error("[Addons] Addon '{}' threw during {}", la.modId, phase, t); + return false; + } finally { + currentAddonId = null; + currentAddonName = null; + } + } + + private static void rollbackRegistrations(String addonId) { + if (addonId == null || addonId.isBlank()) return; + AddonReport report = REPORTS.get(addonId); + rollbackStep(addonId, report, "macro actions", () -> autismclient.api.macro.MacroActionRegistry.unregisterAddon(addonId)); + rollbackStep(addonId, report, "presets", () -> autismclient.api.macro.MacroPresetRegistry.unregisterAddon(addonId)); + rollbackStep(addonId, report, "events", () -> autismclient.api.event.AddonEvents.unregisterAddon(addonId)); + rollbackStep(addonId, report, "hud", () -> autismclient.api.hud.HudElements.unregisterAddon(addonId)); + rollbackStep(addonId, report, "modules", () -> autismclient.modules.PackModuleRegistry.unregisterAddonModules(addonId)); + rollbackStep(addonId, report, "commands", () -> autismclient.commands.AutismCommands.unregisterAddonCommands(addonId)); + if (report != null) report.addRollback("Rolled back partial registrations after load failure."); + } + + private static void rollbackStep(String addonId, AddonReport report, String kind, Runnable body) { + try { + body.run(); + } catch (Throwable t) { + String detail = "Failed to rollback " + kind + ": " + shortError(t); + if (report != null) report.addRollback(detail); + AutismClientAddon.LOG.warn("[Addons] Addon '{}' rollback failed for {}", addonId, kind, t); + } + } + + private static AddonReport ensureReport(ModMetadata meta) { + String modId = meta.getId(); + AddonReport existing = REPORTS.get(modId); + if (existing != null) return existing; + AddonReport report = new AddonReport( + modId, + meta.getName() == null || meta.getName().isBlank() ? modId : meta.getName(), + meta.getVersion().getFriendlyString(), + meta.getAuthors().stream().map(Person::getName).collect(Collectors.joining(", ")), + metadataColor(meta, 0xFFAA66FF) + ); + REPORTS.put(modId, report); + return report; + } + + private static void applyMetadata(AutismAddon addon, ModMetadata meta) { + addon.name = meta.getName(); + addon.authors = meta.getAuthors().stream().map(Person::getName).collect(Collectors.joining(", ")); + addon.color = metadataColor(meta, addon.color); + } + + private static int metadataColor(ModMetadata meta, int fallback) { + CustomValue color = meta.containsCustomValue("autism:color") ? meta.getCustomValue("autism:color") : null; + if (color != null && color.getType() == CustomValue.CvType.STRING) { + return parseColor(color.getAsString(), fallback); + } + return fallback; + } + + private static int parseColor(String raw, int fallback) { + try { + String[] parts = raw.split(","); + if (parts.length != 3) return fallback; + int r = clamp(Integer.parseInt(parts[0].trim())); + int g = clamp(Integer.parseInt(parts[1].trim())); + int b = clamp(Integer.parseInt(parts[2].trim())); + return 0xFF000000 | (r << 16) | (g << 8) | b; + } catch (Exception e) { + return fallback; + } + } + + private static int clamp(int v) { return Math.max(0, Math.min(255, v)); } + + public static String currentAddonId() { + return currentAddonId; + } + + public static boolean isLifecycleActive() { + return currentAddonId != null && !currentAddonId.isBlank(); + } + + public static String currentAddonName() { + return currentAddonName; + } + + public static String normalizedAddonName(String name, String fallback) { + String value = name == null ? "" : name.replaceAll("\\s+", " ").trim(); + if (value.isBlank()) value = fallback == null ? "" : fallback.replaceAll("\\s+", " ").trim(); + return value.isBlank() ? "Addon" : value; + } + + public static String compactAddonName(String name, String fallback) { + String value = normalizedAddonName(name, fallback); + if (value.length() <= MAX_UI_ADDON_NAME_LENGTH) return value; + return value.substring(0, Math.max(1, MAX_UI_ADDON_NAME_LENGTH - 3)).trim() + "..."; + } + + public static String scopedId(String rawId) { + String raw = rawId == null ? "" : rawId.trim(); + if (raw.contains(":")) return raw; + String owner = currentAddonId; + if (owner == null || owner.isBlank()) return raw; + return owner + ":" + raw; + } + + public static String scopedCategoryLabel(String customLabel) { + String name = currentAddonName; + if (name == null || name.isBlank()) name = currentAddonId; + return compactAddonName(name, currentAddonId); + } + + public static List loaded() { + return Collections.unmodifiableList(LOADED); + } + + public static List reports() { + return Collections.unmodifiableList(new ArrayList<>(REPORTS.values())); + } + + public static AddonReport report(String addonId) { + return addonId == null ? null : REPORTS.get(addonId); + } + + public static File modsFolder() { + return FabricLoader.getInstance().getGameDir().resolve("mods").toFile(); + } + + public static boolean isDisabledOnRestart(String addonId) { + if (addonId == null || addonId.isBlank()) return false; + AutismConfig config = AutismConfig.getGlobal(); + if (config.disabledAddonIds == null) return false; + String needle = addonId.trim().toLowerCase(Locale.ROOT); + for (String id : config.disabledAddonIds) { + if (id != null && needle.equals(id.trim().toLowerCase(Locale.ROOT))) return true; + } + return false; + } + + public static void setDisabledOnRestart(String addonId, boolean disabled) { + if (addonId == null || addonId.isBlank()) return; + AutismConfig config = AutismConfig.getGlobal(); + if (config.disabledAddonIds == null) config.disabledAddonIds = new ArrayList<>(); + String normalized = addonId.trim(); + boolean currently = isDisabledOnRestart(normalized); + if (disabled && !currently) { + config.disabledAddonIds.add(normalized); + } else if (!disabled && currently) { + config.disabledAddonIds.removeIf(id -> id != null && normalized.equalsIgnoreCase(id.trim())); + } + config.save(); + } + + public static void recordAcceptedRegistration(String kind, String id) { + AddonReport report = REPORTS.get(currentAddonId); + if (report != null) report.recordAccepted(kind, id); + } + + public static void recordRejectedRegistration(String addonId, String kind, String id, String reason) { + AddonReport report = REPORTS.get(addonId == null || addonId.isBlank() ? currentAddonId : addonId); + if (report != null) report.recordRejected(kind, id, reason); + } + + public static void recordRuntimeError(String addonId, String detail) { + AddonReport report = REPORTS.get(addonId); + if (report != null) report.recordRuntimeError(detail); + } + + public static void surfaceFailuresOnJoin() { + if (failuresSurfaced) return; + int issues = issueCount(); + if (issues <= 0) return; + failuresSurfaced = true; + AutismNotifications.warning(issues + " addon issue(s). Open Addons for details."); + } + + private static int issueCount() { + int count = 0; + for (AddonReport report : REPORTS.values()) { + if (report.status().isIssue()) count++; + } + return count; + } + + private static String shortError(Throwable t) { + if (t == null) return "Unknown error"; + String msg = t.getMessage(); + return msg == null || msg.isBlank() ? t.getClass().getSimpleName() : t.getClass().getSimpleName() + ": " + msg; + } + + private record LoadedAddon(String modId, AutismAddon addon) {} + + public enum AddonLoadStatus { + LOADED("Loaded", false), + DISABLED("Disabled on restart", false), + FAILED("Failed", true), + NEEDS_NEWER_API("Needs newer API", true), + MISSING_API_VERSION("Missing apiVersion", true); + + private final String label; + private final boolean issue; + + AddonLoadStatus(String label, boolean issue) { + this.label = label; + this.issue = issue; + } + + public String label() { return label; } + public boolean isIssue() { return issue; } + } + + public static final class AddonReport { + private final String modId; + private String name; + private String version; + private String authors; + private int color; + private int apiVersion = -1; + private int hostApiVersion = ApiVersion.CURRENT; + private AddonLoadStatus status = AddonLoadStatus.FAILED; + private String phase = "discovered"; + private String failureReason = ""; + private final Map accepted = new LinkedHashMap<>(); + private final Map rejected = new LinkedHashMap<>(); + private final List rejectionDetails = new ArrayList<>(); + private final List rollbackDetails = new ArrayList<>(); + private int runtimeErrors; + private String lastRuntimeError = ""; + + private AddonReport(String modId, String name, String version, String authors, int color) { + this.modId = modId == null ? "" : modId; + this.name = name == null || name.isBlank() ? this.modId : name; + this.version = version == null ? "" : version; + this.authors = authors == null ? "" : authors; + this.color = color; + } + + private void copyRuntimeMetadata(AutismAddon addon) { + if (addon == null) return; + if (addon.name != null && !addon.name.isBlank()) this.name = addon.name; + if (addon.authors != null && !addon.authors.isBlank()) this.authors = addon.authors; + this.color = addon.color; + } + + private void setApiVersion(int apiVersion) { + this.apiVersion = apiVersion; + } + + private void setPhase(String phase) { + this.phase = phase == null || phase.isBlank() ? this.phase : phase; + } + + private void markLoaded() { + this.status = AddonLoadStatus.LOADED; + this.phase = "loaded"; + this.failureReason = ""; + } + + private void markDisabled(String reason) { + this.status = AddonLoadStatus.DISABLED; + this.phase = "disabled"; + this.failureReason = reason == null ? "" : reason; + } + + private void markFailed(String phase, String reason) { + this.status = AddonLoadStatus.FAILED; + this.phase = phase == null || phase.isBlank() ? "failed" : phase; + this.failureReason = reason == null ? "" : reason; + } + + private void markNeedsNewerApi(String reason) { + this.status = AddonLoadStatus.NEEDS_NEWER_API; + this.phase = "apiVersion"; + this.failureReason = reason == null ? "" : reason; + } + + private void markMissingApiVersion(String reason) { + this.status = AddonLoadStatus.MISSING_API_VERSION; + this.phase = "apiVersion"; + this.failureReason = reason == null ? "" : reason; + } + + private void recordAccepted(String kind, String id) { + String key = kind == null || kind.isBlank() ? "item" : kind; + accepted.merge(key, 1, Integer::sum); + } + + private void recordRejected(String kind, String id, String reason) { + String key = kind == null || kind.isBlank() ? "item" : kind; + rejected.merge(key, 1, Integer::sum); + String detail = key + (id == null || id.isBlank() ? "" : " " + id) + ": " + + (reason == null || reason.isBlank() ? "rejected" : reason); + rejectionDetails.add(detail); + } + + private void addRollback(String detail) { + if (detail != null && !detail.isBlank()) rollbackDetails.add(detail); + } + + private void recordRuntimeError(String detail) { + runtimeErrors++; + lastRuntimeError = detail == null || detail.isBlank() ? "Runtime error" : detail; + } + + public String modId() { return modId; } + public String name() { return name; } + public String version() { return version; } + public String authors() { return authors; } + public int color() { return color; } + public int apiVersion() { return apiVersion; } + public int hostApiVersion() { return hostApiVersion; } + public AddonLoadStatus status() { return status; } + public String phase() { return phase; } + public String failureReason() { return failureReason; } + public int runtimeErrors() { return runtimeErrors; } + public String lastRuntimeError() { return lastRuntimeError; } + public Map accepted() { return Collections.unmodifiableMap(accepted); } + public Map rejected() { return Collections.unmodifiableMap(rejected); } + public List rejectionDetails() { return Collections.unmodifiableList(rejectionDetails); } + public List rollbackDetails() { return Collections.unmodifiableList(rollbackDetails); } + + public int acceptedTotal() { + int total = 0; + for (int value : accepted.values()) total += value; + return total; + } + + public int rejectedTotal() { + int total = 0; + for (int value : rejected.values()) total += value; + return total; + } + + public String summaryCounts() { + if (accepted.isEmpty()) return "No registered extensions"; + List parts = new ArrayList<>(); + for (Map.Entry entry : accepted.entrySet()) { + parts.add(entry.getValue() + " " + entry.getKey()); + } + return String.join(", ", parts); + } + + public String copyReport() { + StringBuilder sb = new StringBuilder(); + sb.append("Addon: ").append(name).append('\n'); + sb.append("ID: ").append(modId).append('\n'); + if (!version.isBlank()) sb.append("Version: ").append(version).append('\n'); + if (!authors.isBlank()) sb.append("Authors: ").append(authors).append('\n'); + sb.append("Status: ").append(status.label()).append('\n'); + sb.append("Phase: ").append(phase).append('\n'); + sb.append("API: addon v").append(apiVersion < 0 ? "unknown" : Integer.toString(apiVersion)) + .append(", host v").append(hostApiVersion).append('\n'); + sb.append("Disabled on restart: ").append(isDisabledOnRestart(modId)).append('\n'); + if (!failureReason.isBlank()) sb.append("Reason: ").append(failureReason).append('\n'); + sb.append("Accepted: ").append(accepted).append('\n'); + if (!rejected.isEmpty()) sb.append("Rejected: ").append(rejected).append('\n'); + for (String detail : rejectionDetails) sb.append("- ").append(detail).append('\n'); + for (String detail : rollbackDetails) sb.append("- ").append(detail).append('\n'); + if (runtimeErrors > 0) { + sb.append("Runtime errors: ").append(runtimeErrors).append('\n'); + sb.append("Last runtime error: ").append(lastRuntimeError).append('\n'); + } + return sb.toString(); + } + } +} diff --git a/src/main/java/autismclient/api/AddonRegistrationResult.java b/src/main/java/autismclient/api/AddonRegistrationResult.java new file mode 100644 index 0000000..bc102a3 --- /dev/null +++ b/src/main/java/autismclient/api/AddonRegistrationResult.java @@ -0,0 +1,15 @@ +package autismclient.api; + +public record AddonRegistrationResult(boolean accepted, String kind, String id, String reason) { + public static AddonRegistrationResult accepted(String kind, String id) { + return new AddonRegistrationResult(true, clean(kind), clean(id), "Accepted"); + } + + public static AddonRegistrationResult rejected(String kind, String id, String reason) { + return new AddonRegistrationResult(false, clean(kind), clean(id), clean(reason)); + } + + private static String clean(String value) { + return value == null || value.isBlank() ? "" : value.trim(); + } +} diff --git a/src/main/java/autismclient/api/ApiVersion.java b/src/main/java/autismclient/api/ApiVersion.java new file mode 100644 index 0000000..418b0f2 --- /dev/null +++ b/src/main/java/autismclient/api/ApiVersion.java @@ -0,0 +1,8 @@ +package autismclient.api; + +public final class ApiVersion { + + public static final int CURRENT = 1; + + private ApiVersion() {} +} diff --git a/src/main/java/autismclient/api/AutismAddon.java b/src/main/java/autismclient/api/AutismAddon.java new file mode 100644 index 0000000..5bb0b4f --- /dev/null +++ b/src/main/java/autismclient/api/AutismAddon.java @@ -0,0 +1,18 @@ +package autismclient.api; + +public abstract class AutismAddon { + + public String name = ""; + + public String authors = ""; + + public int color = 0xFFFFFFFF; + + public abstract int apiVersion(); + + public void onRegisterCategories() {} + + public abstract void onInitialize(); + + public abstract String getPackage(); +} diff --git a/src/main/java/autismclient/api/AutismAddons.java b/src/main/java/autismclient/api/AutismAddons.java new file mode 100644 index 0000000..0d03cb1 --- /dev/null +++ b/src/main/java/autismclient/api/AutismAddons.java @@ -0,0 +1,147 @@ +package autismclient.api; + +import autismclient.addons.AddonManager; +import autismclient.api.event.AddonEvents; +import autismclient.api.hud.HudElementProvider; +import autismclient.api.hud.HudElements; +import autismclient.api.macro.MacroActionEntry; +import autismclient.api.macro.MacroActionRegistry; +import autismclient.api.macro.MacroPresetRegistry; +import autismclient.commands.Command; +import autismclient.modules.PackModule; +import autismclient.modules.PackModuleCategory; +import autismclient.modules.PackModuleRegistry; +import autismclient.util.macro.MacroAction; +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.Packet; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public final class AutismAddons { + private AutismAddons() {} + + public static Modules modules() { return Modules.INSTANCE; } + + public static Commands commands() { return Commands.INSTANCE; } + + public static MacroActions macroActions() { return MacroActions.INSTANCE; } + + public static Presets presets() { return Presets.INSTANCE; } + + public static Hud hud() { return Hud.INSTANCE; } + + public static Events events() { return Events.INSTANCE; } + + public static List list() { return AddonManager.loaded(); } + + public static String id(String localId) { return AddonManager.scopedId(localId); } + + public static final class Modules { + static final Modules INSTANCE = new Modules(); + private Modules() {} + + public boolean register(PackModule module) { + return PackModuleRegistry.registerAddonModule(module, AddonManager.currentAddonId()); + } + + public AddonRegistrationResult registerDetailed(PackModule module) { + return PackModuleRegistry.registerAddonModuleDetailed(module, AddonManager.currentAddonId()); + } + + public PackModuleCategory autoCategory() { + return PackModuleCategory.registerAddon(AddonManager.currentAddonId(), + AddonManager.scopedCategoryLabel(null)); + } + + public PackModuleCategory registerCategory(String label) { + return PackModuleCategory.registerAddon(AddonManager.currentAddonId(), + AddonManager.scopedCategoryLabel(label)); + } + } + + public static final class Commands { + static final Commands INSTANCE = new Commands(); + private Commands() {} + + public void register(Command command) { + autismclient.commands.AutismCommands.registerAddonCommand(command, AddonManager.currentAddonId()); + } + + public AddonRegistrationResult registerDetailed(Command command) { + return autismclient.commands.AutismCommands.registerAddonCommandDetailed(command, AddonManager.currentAddonId()); + } + } + + public static final class MacroActions { + static final MacroActions INSTANCE = new MacroActions(); + private MacroActions() {} + + public boolean register(MacroActionEntry entry) { + return MacroActionRegistry.register(entry); + } + + public AddonRegistrationResult registerDetailed(MacroActionEntry entry) { + return MacroActionRegistry.registerDetailed(entry); + } + + public void registerCategory(String id, String label, int color) { + MacroActionRegistry.registerScopedCategory(id, label, color); + } + } + + public static final class Presets { + static final Presets INSTANCE = new Presets(); + private Presets() {} + + public void registerCategory(String id, String label) { + MacroPresetRegistry.registerScopedCategory(id, label); + } + + public void register(String label, String tip, Supplier> builder) { + MacroPresetRegistry.register(MacroPresetRegistry.ensureScopedCategory(null), label, tip, builder); + } + + public AddonRegistrationResult registerDetailed(String label, String tip, Supplier> builder) { + return MacroPresetRegistry.registerDetailed(MacroPresetRegistry.ensureScopedCategory(null), label, tip, builder); + } + + public void register(String categoryId, String label, String tip, Supplier> builder) { + MacroPresetRegistry.register(MacroPresetRegistry.ensureScopedCategory(categoryId), label, tip, builder); + } + + public AddonRegistrationResult registerDetailed(String categoryId, String label, String tip, Supplier> builder) { + return MacroPresetRegistry.registerDetailed(MacroPresetRegistry.ensureScopedCategory(categoryId), label, tip, builder); + } + } + + public static final class Hud { + static final Hud INSTANCE = new Hud(); + private Hud() {} + + public boolean register(HudElementProvider provider) { + return HudElements.register(provider); + } + + public AddonRegistrationResult registerDetailed(HudElementProvider provider) { + return HudElements.registerDetailed(provider); + } + } + + public static final class Events { + static final Events INSTANCE = new Events(); + private Events() {} + + public void onTick(Consumer listener) { AddonEvents.onTick(listener); } + + public void onPacketSend(Predicate> listener) { AddonEvents.onPacketSend(listener); } + + public void onPacketReceive(Consumer> listener) { AddonEvents.onPacketReceive(listener); } + + public void onGameJoin(Runnable listener) { AddonEvents.onGameJoin(listener); } + + public void onGameLeft(Runnable listener) { AddonEvents.onGameLeft(listener); } + } +} diff --git a/src/main/java/autismclient/api/SimpleAddon.java b/src/main/java/autismclient/api/SimpleAddon.java new file mode 100644 index 0000000..ae7bea8 --- /dev/null +++ b/src/main/java/autismclient/api/SimpleAddon.java @@ -0,0 +1,84 @@ +package autismclient.api; + +import autismclient.api.macro.MacroActionEntry; +import autismclient.api.macro.MacroPresetRegistry; +import autismclient.api.macro.SimpleAction; +import autismclient.api.macro.SimpleCondition; +import autismclient.modules.PackModule; +import autismclient.util.macro.MacroAction; +import net.minecraft.client.Minecraft; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public abstract class SimpleAddon extends AutismAddon { + private final int apiVersion; + private final String rootPackage; + + protected SimpleAddon(int apiVersion, String rootPackage) { + this.apiVersion = apiVersion; + this.rootPackage = rootPackage == null ? "" : rootPackage; + } + + @Override + public final int apiVersion() { + return apiVersion; + } + + @Override + public final String getPackage() { + return rootPackage; + } + + @Override + public final void onInitialize() { + initialize(); + } + + protected abstract void initialize(); + + protected final String id(String localId) { + return AutismAddons.id(localId); + } + + protected final AddonRegistrationResult registerModule(PackModule module) { + return AutismAddons.modules().registerDetailed(module); + } + + protected final AddonRegistrationResult registerAction(MacroActionEntry entry) { + return AutismAddons.macroActions().registerDetailed(entry); + } + + protected final AddonRegistrationResult registerPreset(String label, String tip, Supplier> builder) { + return AutismAddons.presets().registerDetailed(label, tip, builder); + } + + protected final MacroActionEntry simpleAction( + String localId, + String label, + String tip, + String icon, + Consumer runner + ) { + String typeId = id(localId); + return MacroActionEntry.builder(typeId, () -> new SimpleAction(typeId, label, icon, runner)) + .picker(label, tip) + .build(); + } + + protected final MacroActionEntry simpleCondition( + String localId, + String label, + String tip, + String status, + String icon, + Predicate predicate + ) { + String typeId = id(localId); + return MacroActionEntry.builder(typeId, () -> new SimpleCondition(typeId, label, status, icon, predicate)) + .condition(label, tip) + .build(); + } +} diff --git a/src/main/java/autismclient/api/event/AddonEvents.java b/src/main/java/autismclient/api/event/AddonEvents.java new file mode 100644 index 0000000..271100c --- /dev/null +++ b/src/main/java/autismclient/api/event/AddonEvents.java @@ -0,0 +1,145 @@ +package autismclient.api.event; + +import autismclient.AutismClientAddon; +import autismclient.addons.AddonManager; +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.Packet; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public final class AddonEvents { + private static final List> TICK = new CopyOnWriteArrayList<>(); + private static final List>> PACKET_SEND = new CopyOnWriteArrayList<>(); + private static final List>> PACKET_RECEIVE = new CopyOnWriteArrayList<>(); + private static final List GAME_JOIN = new CopyOnWriteArrayList<>(); + private static final List GAME_LEFT = new CopyOnWriteArrayList<>(); + + private static final long ERROR_LOG_INTERVAL_MS = 5_000L; + private static final Map LAST_ERROR_LOG_MS = new ConcurrentHashMap<>(); + + private AddonEvents() {} + + public static void onTick(Consumer listener) { + String owner = ownerOrReject("tick"); + if (owner != null && listener != null) { + TICK.add(new OwnedConsumer<>(owner, listener)); + AddonManager.recordAcceptedRegistration("event", "tick"); + } + } + + public static void onPacketSend(Predicate> listener) { + String owner = ownerOrReject("packetSend"); + if (owner != null && listener != null) { + PACKET_SEND.add(new OwnedPredicate<>(owner, listener)); + AddonManager.recordAcceptedRegistration("event", "packetSend"); + } + } + + public static void onPacketReceive(Consumer> listener) { + String owner = ownerOrReject("packetReceive"); + if (owner != null && listener != null) { + PACKET_RECEIVE.add(new OwnedConsumer<>(owner, listener)); + AddonManager.recordAcceptedRegistration("event", "packetReceive"); + } + } + + public static void onGameJoin(Runnable listener) { + String owner = ownerOrReject("gameJoin"); + if (owner != null && listener != null) { + GAME_JOIN.add(new OwnedRunnable(owner, listener)); + AddonManager.recordAcceptedRegistration("event", "gameJoin"); + } + } + + public static void onGameLeft(Runnable listener) { + String owner = ownerOrReject("gameLeft"); + if (owner != null && listener != null) { + GAME_LEFT.add(new OwnedRunnable(owner, listener)); + AddonManager.recordAcceptedRegistration("event", "gameLeft"); + } + } + + private static String ownerOrReject(String event) { + String owner = AddonManager.currentAddonId(); + if (owner == null || owner.isBlank()) { + AutismClientAddon.LOG.warn("[Addons] Rejecting {} listener outside an addon lifecycle", event); + AddonManager.recordRejectedRegistration(owner, "event", event, "registration outside an addon lifecycle"); + return null; + } + return owner; + } + + public static void unregisterAddon(String addonId) { + if (addonId == null || addonId.isBlank()) return; + TICK.removeIf(listener -> addonId.equals(listener.owner())); + PACKET_SEND.removeIf(listener -> addonId.equals(listener.owner())); + PACKET_RECEIVE.removeIf(listener -> addonId.equals(listener.owner())); + GAME_JOIN.removeIf(listener -> addonId.equals(listener.owner())); + GAME_LEFT.removeIf(listener -> addonId.equals(listener.owner())); + LAST_ERROR_LOG_MS.keySet().removeIf(key -> key.startsWith(addonId + ":")); + } + + public static void fireTick(Minecraft mc) { + if (TICK.isEmpty()) return; + for (OwnedConsumer l : TICK) { + try { l.listener().accept(mc); } catch (Throwable t) { logThrottled(l.owner(), "tick", t); } + } + } + + public static boolean hasTickListeners() { + return !TICK.isEmpty(); + } + + public static boolean hasPacketListeners() { + return !PACKET_SEND.isEmpty() || !PACKET_RECEIVE.isEmpty(); + } + + public static boolean firePacketSend(Packet packet) { + if (PACKET_SEND.isEmpty()) return false; + boolean cancel = false; + for (OwnedPredicate> l : PACKET_SEND) { + try { cancel |= l.listener().test(packet); } catch (Throwable t) { logThrottled(l.owner(), "packetSend", t); } + } + return cancel; + } + + public static void firePacketReceive(Packet packet) { + if (PACKET_RECEIVE.isEmpty()) return; + for (OwnedConsumer> l : PACKET_RECEIVE) { + try { l.listener().accept(packet); } catch (Throwable t) { logThrottled(l.owner(), "packetReceive", t); } + } + } + + public static void fireGameJoin() { + if (GAME_JOIN.isEmpty()) return; + for (OwnedRunnable l : GAME_JOIN) { + try { l.listener().run(); } catch (Throwable t) { logThrottled(l.owner(), "gameJoin", t); } + } + } + + public static void fireGameLeft() { + if (GAME_LEFT.isEmpty()) return; + for (OwnedRunnable l : GAME_LEFT) { + try { l.listener().run(); } catch (Throwable t) { logThrottled(l.owner(), "gameLeft", t); } + } + } + + private static void logThrottled(String owner, String event, Throwable t) { + long now = System.currentTimeMillis(); + String key = owner + ":" + event + ":" + t.getClass().getName(); + Long last = LAST_ERROR_LOG_MS.get(key); + if (last != null && now - last < ERROR_LOG_INTERVAL_MS) return; + LAST_ERROR_LOG_MS.put(key, now); + AddonManager.recordRuntimeError(owner, event + " listener threw " + t.getClass().getSimpleName()); + AutismClientAddon.LOG.warn("[Addons] Addon '{}' {} event listener threw", owner, event, t); + } + + private record OwnedConsumer(String owner, Consumer listener) {} + private record OwnedPredicate(String owner, Predicate listener) {} + private record OwnedRunnable(String owner, Runnable listener) {} +} diff --git a/src/main/java/autismclient/api/hud/HudElementProvider.java b/src/main/java/autismclient/api/hud/HudElementProvider.java new file mode 100644 index 0000000..7d19025 --- /dev/null +++ b/src/main/java/autismclient/api/hud/HudElementProvider.java @@ -0,0 +1,27 @@ +package autismclient.api.hud; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +public interface HudElementProvider { + + String id(); + + String label(); + + default String description() { return ""; } + + int width(); + + int height(); + + void render(GuiGraphicsExtractor ctx, Font font, int x, int y, float alpha); + + default boolean defaultEnabled() { return false; } + + default String defaultAnchor() { return "TOP_LEFT"; } + + default int defaultX() { return 0; } + + default int defaultY() { return 0; } +} diff --git a/src/main/java/autismclient/api/hud/HudElements.java b/src/main/java/autismclient/api/hud/HudElements.java new file mode 100644 index 0000000..4a0cd13 --- /dev/null +++ b/src/main/java/autismclient/api/hud/HudElements.java @@ -0,0 +1,104 @@ +package autismclient.api.hud; + +import autismclient.AutismClientAddon; +import autismclient.api.AddonRegistrationResult; +import autismclient.addons.AddonManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class HudElements { + private static final Map PROVIDERS = new LinkedHashMap<>(); + private static final Map OWNERS = new LinkedHashMap<>(); + private static int revision; + + private HudElements() {} + + public static int revision() { + return revision; + } + + public static boolean register(HudElementProvider provider) { + return registerDetailed(provider).accepted(); + } + + public static AddonRegistrationResult registerDetailed(HudElementProvider provider) { + if (provider == null) return AddonRegistrationResult.rejected("hud", "", "provider was null"); + String ownerAddonId = AddonManager.currentAddonId(); + if (ownerAddonId == null || ownerAddonId.isBlank()) { + return reject(ownerAddonId, "", "registration outside an addon lifecycle"); + } + String id = provider.id(); + if (id == null || !id.contains(":")) { + return reject(ownerAddonId, id, "non-namespaced id"); + } + if (!id.startsWith(ownerAddonId + ":")) { + return reject(ownerAddonId, id, "foreign namespace"); + } + if (PROVIDERS.containsKey(id)) { + return reject(ownerAddonId, id, "duplicate id"); + } + AddonRegistrationResult sizeCheck = validateSize(ownerAddonId, id, provider); + if (!sizeCheck.accepted()) return sizeCheck; + PROVIDERS.put(id, provider); + OWNERS.put(id, ownerAddonId); + revision++; + AddonManager.recordAcceptedRegistration("hud", id); + return AddonRegistrationResult.accepted("hud", id); + } + + private static AddonRegistrationResult validateSize(String ownerAddonId, String id, HudElementProvider provider) { + int width; + int height; + try { + width = provider.width(); + height = provider.height(); + } catch (Throwable t) { + AutismClientAddon.LOG.warn("[Hud] Rejecting HUD element '{}': sizing threw", id, t); + AddonManager.recordRejectedRegistration(ownerAddonId, "hud", id, "sizing threw " + t.getClass().getSimpleName()); + return AddonRegistrationResult.rejected("hud", id, "sizing threw " + t.getClass().getSimpleName()); + } + if (width <= 0) return reject(ownerAddonId, id, "width must be positive"); + if (height <= 0) return reject(ownerAddonId, id, "height must be positive"); + return AddonRegistrationResult.accepted("hud", id); + } + + private static AddonRegistrationResult reject(String ownerAddonId, String id, String reason) { + AutismClientAddon.LOG.warn("[Hud] Rejecting HUD element '{}': {}", id, reason); + AddonManager.recordRejectedRegistration(ownerAddonId, "hud", id, reason); + return AddonRegistrationResult.rejected("hud", id, reason); + } + + public static void unregisterAddon(String addonId) { + if (addonId == null || addonId.isBlank()) return; + List remove = new ArrayList<>(); + for (Map.Entry owner : OWNERS.entrySet()) { + if (addonId.equals(owner.getValue())) remove.add(owner.getKey()); + } + if (remove.isEmpty()) return; + for (String id : remove) { + PROVIDERS.remove(id); + OWNERS.remove(id); + } + revision++; + } + + public static HudElementProvider get(String id) { + return id == null ? null : PROVIDERS.get(id); + } + + public static boolean isAddon(String id) { + return id != null && PROVIDERS.containsKey(id); + } + + public static List ids() { + return Collections.unmodifiableList(new ArrayList<>(PROVIDERS.keySet())); + } + + public static boolean isEmpty() { + return PROVIDERS.isEmpty(); + } +} diff --git a/src/main/java/autismclient/api/macro/ActionSchema.java b/src/main/java/autismclient/api/macro/ActionSchema.java new file mode 100644 index 0000000..2769f6f --- /dev/null +++ b/src/main/java/autismclient/api/macro/ActionSchema.java @@ -0,0 +1,81 @@ +package autismclient.api.macro; + +import autismclient.gui.macro.editor.ActionFieldSchema; + +public final class ActionSchema { + private final ActionFieldSchema internal; + + private ActionSchema(ActionFieldSchema internal) { + this.internal = internal; + } + + public ActionFieldSchema internal() { + return internal; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final ActionFieldSchema.Builder delegate = ActionFieldSchema.builder(); + + private Builder() {} + + public Builder toggle(String key, String label) { delegate.toggle(key, label); return this; } + + public Builder number(String key, String label) { delegate.number(key, label); return this; } + + public Builder decimal(String key, String label) { delegate.decimal(key, label); return this; } + + public Builder text(String key, String label) { delegate.text(key, label); return this; } + + public Builder macroSelect(String key, String label) { delegate.macroSelect(key, label); return this; } + + public Builder enumField(String key, String label, String... options) { delegate.enumField(key, label, options); return this; } + + public Builder stringList(String key, String label) { delegate.stringList(key, label); return this; } + + public Builder slot(String key, String label) { delegate.slot(key, label); return this; } + + public Builder targetSummary(String key, String label) { delegate.targetSummary(key, label); return this; } + + public Builder capturePacketClick(String key, String label) { delegate.capturePacketClick(key, label); return this; } + + public Builder blockPos(String key, String label) { delegate.blockPos(key, label); return this; } + + public Builder range(int min, int max) { delegate.range(min, max); return this; } + + public Builder decRange(double min, double max) { delegate.decRange(min, max); return this; } + + public Builder showWhen(String key) { delegate.showWhen(key); return this; } + + public Builder hideWhen(String key) { delegate.hideWhen(key); return this; } + + public Builder showWhenEnum(String key, String value) { delegate.showWhenEnum(key, value); return this; } + + public Builder hideWhenEnum(String key, String value) { delegate.hideWhenEnum(key, value); return this; } + + public Builder addLabel(String label) { delegate.addLabel(label); return this; } + + public Builder xyzKeys(String... keys) { delegate.xyzKeys(keys); return this; } + + public Builder xyzDouble(boolean value) { delegate.xyzDouble(value); return this; } + + public Builder captureBlock() { delegate.captureBlock(); return this; } + + public Builder captureEntity() { delegate.captureEntity(); return this; } + + public Builder captureCatalog() { delegate.captureCatalog(); return this; } + + public Builder captureItemSlot() { delegate.captureItemSlot(); return this; } + + public Builder capturePacketName() { delegate.capturePacketName(); return this; } + + public Builder exclusiveWith(String... keys) { delegate.exclusiveWith(keys); return this; } + + public ActionSchema build() { + return new ActionSchema(delegate.build()); + } + } +} diff --git a/src/main/java/autismclient/api/macro/AddonAction.java b/src/main/java/autismclient/api/macro/AddonAction.java new file mode 100644 index 0000000..0ac6625 --- /dev/null +++ b/src/main/java/autismclient/api/macro/AddonAction.java @@ -0,0 +1,61 @@ +package autismclient.api.macro; + +import autismclient.util.macro.MacroAction; +import net.minecraft.nbt.CompoundTag; + +public abstract class AddonAction implements MacroAction { + private final String typeId; + + protected AddonAction(String typeId) { + this.typeId = typeId; + } + + protected abstract void run(net.minecraft.client.Minecraft mc); + + protected void save(CompoundTag tag) {} + + protected void load(CompoundTag tag) {} + + protected ActionSchema schema() { + return null; + } + + @Override + public final void execute(net.minecraft.client.Minecraft mc) { + run(mc); + } + + @Override + public final String getTypeId() { + return typeId; + } + + @Override + public final CompoundTag toTag() { + CompoundTag tag = new CompoundTag(); + tag.putString("type", typeId); + save(tag); + return tag; + } + + @Override + public final void fromTag(CompoundTag tag) { + load(tag); + } + + protected static void putString(CompoundTag tag, String key, String value) { tag.putString(key, value == null ? "" : value); } + protected static void putInt(CompoundTag tag, String key, int value) { tag.putInt(key, value); } + protected static void putDouble(CompoundTag tag, String key, double value) { tag.putDouble(key, value); } + protected static void putBool(CompoundTag tag, String key, boolean value) { tag.putBoolean(key, value); } + + protected static String getString(CompoundTag tag, String key, String def) { return tag.getStringOr(key, def); } + protected static int getInt(CompoundTag tag, String key, int def) { return tag.getIntOr(key, def); } + protected static double getDouble(CompoundTag tag, String key, double def) { return tag.getDoubleOr(key, def); } + protected static boolean getBool(CompoundTag tag, String key, boolean def) { return tag.getBooleanOr(key, def); } + + public static ActionSchema schemaOf(MacroAction action) { + return action instanceof AddonAction a ? a.schema() + : action instanceof AddonContextAction c ? c.schemaInternal() + : null; + } +} diff --git a/src/main/java/autismclient/api/macro/AddonCondition.java b/src/main/java/autismclient/api/macro/AddonCondition.java new file mode 100644 index 0000000..469507c --- /dev/null +++ b/src/main/java/autismclient/api/macro/AddonCondition.java @@ -0,0 +1,10 @@ +package autismclient.api.macro; + +import net.minecraft.client.Minecraft; + +public abstract class AddonCondition { + + public abstract boolean check(Minecraft mc); + + public void onComplete() {} +} diff --git a/src/main/java/autismclient/api/macro/AddonContextAction.java b/src/main/java/autismclient/api/macro/AddonContextAction.java new file mode 100644 index 0000000..0a0fcf5 --- /dev/null +++ b/src/main/java/autismclient/api/macro/AddonContextAction.java @@ -0,0 +1,51 @@ +package autismclient.api.macro; + +import net.minecraft.nbt.CompoundTag; + +public abstract class AddonContextAction implements ContextMacroAction { + private final String typeId; + + protected AddonContextAction(String typeId) { + this.typeId = typeId; + } + + protected void save(CompoundTag tag) {} + + protected void load(CompoundTag tag) {} + + protected ActionSchema schema() { + return null; + } + + ActionSchema schemaInternal() { + return schema(); + } + + @Override + public final String getTypeId() { + return typeId; + } + + @Override + public final CompoundTag toTag() { + CompoundTag tag = new CompoundTag(); + tag.putString("type", typeId); + save(tag); + return tag; + } + + @Override + public final void fromTag(CompoundTag tag) { + load(tag); + } + + protected static void putString(CompoundTag tag, String key, String value) { tag.putString(key, value == null ? "" : value); } + protected static void putInt(CompoundTag tag, String key, int value) { tag.putInt(key, value); } + protected static void putDouble(CompoundTag tag, String key, double value) { tag.putDouble(key, value); } + protected static void putBool(CompoundTag tag, String key, boolean value) { tag.putBoolean(key, value); } + + protected static String getString(CompoundTag tag, String key, String def) { return tag.getStringOr(key, def); } + protected static int getInt(CompoundTag tag, String key, int def) { return tag.getIntOr(key, def); } + protected static double getDouble(CompoundTag tag, String key, double def) { return tag.getDoubleOr(key, def); } + protected static boolean getBool(CompoundTag tag, String key, boolean def) { return tag.getBooleanOr(key, def); } +} diff --git a/src/main/java/autismclient/api/macro/ContextMacroAction.java b/src/main/java/autismclient/api/macro/ContextMacroAction.java new file mode 100644 index 0000000..28d1b2c --- /dev/null +++ b/src/main/java/autismclient/api/macro/ContextMacroAction.java @@ -0,0 +1,14 @@ +package autismclient.api.macro; + +import autismclient.util.macro.MacroAction; +import net.minecraft.client.Minecraft; + +public interface ContextMacroAction extends MacroAction { + + void run(MacroExecutionContext ctx) throws InterruptedException; + + @Override + default void execute(Minecraft mc) { + + } +} diff --git a/src/main/java/autismclient/api/macro/MacroActionEntry.java b/src/main/java/autismclient/api/macro/MacroActionEntry.java new file mode 100644 index 0000000..0b3af51 --- /dev/null +++ b/src/main/java/autismclient/api/macro/MacroActionEntry.java @@ -0,0 +1,103 @@ +package autismclient.api.macro; + +import autismclient.addons.AddonManager; +import autismclient.gui.macro.editor.ActionFieldSchema; +import autismclient.util.macro.MacroAction; + +import java.util.function.Supplier; + +public final class MacroActionEntry { + + public enum Kind { ACTION, CONDITION } + + private final String typeId; + private final Supplier factory; + private final ActionFieldSchema schema; + private String pickerCategory; + private final String pickerLabel; + private final String pickerTip; + private final Kind kind; + + private MacroActionEntry(Builder b) { + this.typeId = b.typeId; + this.factory = b.factory; + this.schema = b.schema; + this.pickerCategory = b.pickerCategory; + this.pickerLabel = b.pickerLabel; + this.pickerTip = b.pickerTip; + this.kind = b.kind; + } + + public String typeId() { return typeId; } + public Supplier factory() { return factory; } + public ActionFieldSchema schema() { return schema; } + + public boolean hasPicker() { return pickerCategory != null && pickerLabel != null; } + + boolean wantsPicker() { return pickerLabel != null; } + public String pickerCategory() { return pickerCategory; } + public String pickerLabel() { return pickerLabel; } + public String pickerTip() { return pickerTip == null ? "" : pickerTip; } + public Kind kind() { return kind; } + public boolean isCondition() { return kind == Kind.CONDITION; } + + void setPickerCategory(String categoryId) { + this.pickerCategory = categoryId; + } + + public static Builder builder(String typeId, Supplier factory) { + return new Builder(typeId, factory); + } + + public static Builder local(String localId, Supplier factory) { + return new Builder(AddonManager.scopedId(localId), factory); + } + + public static final class Builder { + private final String typeId; + private final Supplier factory; + private ActionFieldSchema schema; + private String pickerCategory; + private String pickerLabel; + private String pickerTip; + private Kind kind = Kind.ACTION; + + private Builder(String typeId, Supplier factory) { + this.typeId = typeId; + this.factory = factory; + } + + public Builder schema(ActionSchema schema) { + this.schema = schema == null ? null : schema.internal(); + return this; + } + + public Builder picker(String label, String tip) { + return picker(null, label, tip); + } + + public Builder picker(String categoryId, String label, String tip) { + this.pickerCategory = categoryId; + this.pickerLabel = label; + this.pickerTip = tip; + this.kind = Kind.ACTION; + return this; + } + + public Builder condition(String label, String tip) { + return condition(null, label, tip); + } + + public Builder condition(String categoryId, String label, String tip) { + this.pickerCategory = categoryId; + this.pickerLabel = label; + this.pickerTip = tip; + this.kind = Kind.CONDITION; + return this; + } + + public MacroActionEntry build() { + return new MacroActionEntry(this); + } + } +} diff --git a/src/main/java/autismclient/api/macro/MacroActionRegistry.java b/src/main/java/autismclient/api/macro/MacroActionRegistry.java new file mode 100644 index 0000000..c2c4f3a --- /dev/null +++ b/src/main/java/autismclient/api/macro/MacroActionRegistry.java @@ -0,0 +1,230 @@ +package autismclient.api.macro; + +import autismclient.AutismClientAddon; +import autismclient.api.AddonRegistrationResult; +import autismclient.addons.AddonManager; +import autismclient.gui.macro.editor.ActionFieldSchema; +import autismclient.util.macro.MacroAction; +import autismclient.util.macro.MacroActionType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +public final class MacroActionRegistry { + private static final Map ENTRIES = new LinkedHashMap<>(); + private static final Map CATEGORIES = new LinkedHashMap<>(); + private static final Map ENTRY_OWNERS = new LinkedHashMap<>(); + private static final Map CATEGORY_OWNERS = new LinkedHashMap<>(); + + private static final Map RESOLVED_SCHEMAS = new LinkedHashMap<>(); + + public record ActionCategory(String id, String label, int color) {} + + public static final int ADDON_AUTO_CATEGORY_COLOR = 0xFFAA66FF; + + private static final String ADDON_CAT_PREFIX = "addon:"; + + private MacroActionRegistry() {} + + public static String scopeCategoryId(String rawId) { + if (AddonManager.isLifecycleActive()) { + String addonId = AddonManager.currentAddonId(); + return ADDON_CAT_PREFIX + (addonId == null || addonId.isBlank() ? "shared" : addonId); + } + if (rawId != null && rawId.startsWith(ADDON_CAT_PREFIX)) return rawId; + String addonId = AddonManager.currentAddonId(); + String base = addonId == null || addonId.isBlank() ? "shared" : addonId; + return rawId == null || rawId.isBlank() + ? ADDON_CAT_PREFIX + base + : ADDON_CAT_PREFIX + base + "/" + rawId; + } + + public static String ensureScopedCategory(String rawId) { + String scopedId = scopeCategoryId(rawId); + if (!CATEGORIES.containsKey(scopedId)) { + registerCategoryInternal(scopedId, AddonManager.scopedCategoryLabel(null), ADDON_AUTO_CATEGORY_COLOR, + AddonManager.currentAddonId()); + } + return scopedId; + } + + public static void registerScopedCategory(String rawId, String label, int color) { + String visible = AddonManager.isLifecycleActive() ? AddonManager.scopedCategoryLabel(null) : label; + registerCategoryInternal(scopeCategoryId(rawId), visible, color, + AddonManager.currentAddonId()); + } + + public static void registerCategory(String id, String label, int color) { + registerCategoryInternal(id, label, color, null); + } + + private static void registerCategoryInternal(String id, String label, int color, String ownerAddonId) { + if (id == null || id.isBlank()) return; + CATEGORIES.put(id, new ActionCategory(id, label == null || label.isBlank() ? id : label, color)); + if (ownerAddonId != null && !ownerAddonId.isBlank()) CATEGORY_OWNERS.put(id, ownerAddonId); + } + + public static ActionCategory category(String id) { + return id == null ? null : CATEGORIES.get(id); + } + + public static boolean register(MacroActionEntry entry) { + return registerDetailed(entry).accepted(); + } + + public static AddonRegistrationResult registerDetailed(MacroActionEntry entry) { + if (entry == null) return AddonRegistrationResult.rejected("macro action", "", "entry was null"); + String ownerAddonId = AddonManager.currentAddonId(); + if (ownerAddonId == null || ownerAddonId.isBlank()) { + return reject(ownerAddonId, "macro action", "", "registration outside an addon lifecycle"); + } + + String typeId = entry.typeId(); + if (typeId == null || !typeId.contains(":")) { + return reject(ownerAddonId, "macro action", typeId, "non-namespaced type id"); + } + if (!typeId.startsWith(ownerAddonId + ":")) { + return reject(ownerAddonId, "macro action", typeId, "foreign namespace"); + } + if (entry.factory() == null) { + return reject(ownerAddonId, "macro action", typeId, "missing factory"); + } + if (isBuiltinName(typeId)) { + return reject(ownerAddonId, "macro action", typeId, "collides with a built-in type"); + } + if (ENTRIES.containsKey(typeId)) { + return reject(ownerAddonId, "macro action", typeId, "duplicate type id"); + } + + if (entry.wantsPicker()) { + entry.setPickerCategory(ensureScopedCategory(entry.pickerCategory())); + } + + ActionFieldSchema effectiveSchema = entry.schema(); + MacroAction probe; + try { + probe = entry.factory().get(); + } catch (Throwable t) { + AutismClientAddon.LOG.warn("[MacroActions] Rejecting action '{}' because its factory threw during validation", typeId, t); + return reject(ownerAddonId, "macro action", typeId, "factory threw: " + errorName(t)); + } + if (probe == null) { + return reject(ownerAddonId, "macro action", typeId, "factory returned null"); + } + if (effectiveSchema == null) { + ActionSchema declared = AddonAction.schemaOf(probe); + if (declared != null) effectiveSchema = declared.internal(); + } + String validationError = validateProbe(typeId, entry, probe, effectiveSchema); + if (validationError != null) return reject(ownerAddonId, "macro action", typeId, validationError); + + ENTRIES.put(typeId, entry); + ENTRY_OWNERS.put(typeId, ownerAddonId); + if (effectiveSchema != null) RESOLVED_SCHEMAS.put(typeId, effectiveSchema); + AddonManager.recordAcceptedRegistration("macro action", typeId); + return AddonRegistrationResult.accepted("macro action", typeId); + } + + private static AddonRegistrationResult reject(String ownerAddonId, String kind, String id, String reason) { + AutismClientAddon.LOG.warn("[MacroActions] Rejecting {} '{}': {}", kind, id, reason); + AddonManager.recordRejectedRegistration(ownerAddonId, kind, id, reason); + return AddonRegistrationResult.rejected(kind, id, reason); + } + + private static String validateProbe(String typeId, MacroActionEntry entry, MacroAction probe, ActionFieldSchema schema) { + String writtenType; + net.minecraft.nbt.CompoundTag tag; + try { + tag = probe.toTag(); + writtenType = tag.getStringOr("type", ""); + } catch (Throwable t) { + AutismClientAddon.LOG.warn("[MacroActions] Rejecting '{}' because toTag() threw", typeId, t); + return "toTag() threw: " + errorName(t); + } + if (!typeId.equals(writtenType)) { + return "writes type=\"" + writtenType + "\" instead of \"" + typeId + "\""; + } + if (schema != null) { + for (autismclient.gui.macro.editor.FieldDef field : schema.fields()) { + String key = field.key(); + if (field.type() == autismclient.gui.macro.editor.FieldType.BLOCK_POS) { + for (String axisKey : field.xyzKeys()) { + if (axisKey != null && !axisKey.isEmpty() && !tag.contains(axisKey)) { + return "schema field '" + axisKey + "' is not written by toTag()/save()"; + } + } + } else if (key != null && !key.isEmpty() && !tag.contains(key)) { + return "schema field '" + key + "' is not written by toTag()/save()"; + } + } + } + if (entry.isCondition() && !(probe instanceof ContextMacroAction)) { + return "conditions must implement ContextMacroAction"; + } + return null; + } + + public static void unregisterAddon(String addonId) { + if (addonId == null || addonId.isBlank()) return; + List removeTypes = new ArrayList<>(); + for (Map.Entry owner : ENTRY_OWNERS.entrySet()) { + if (addonId.equals(owner.getValue())) removeTypes.add(owner.getKey()); + } + for (String typeId : removeTypes) { + ENTRIES.remove(typeId); + RESOLVED_SCHEMAS.remove(typeId); + ENTRY_OWNERS.remove(typeId); + } + + List removeCategories = new ArrayList<>(); + for (Map.Entry owner : CATEGORY_OWNERS.entrySet()) { + if (addonId.equals(owner.getValue())) removeCategories.add(owner.getKey()); + } + for (String categoryId : removeCategories) { + CATEGORIES.remove(categoryId); + CATEGORY_OWNERS.remove(categoryId); + } + } + + public static Supplier factory(String typeId) { + MacroActionEntry e = typeId == null ? null : ENTRIES.get(typeId); + return e == null ? null : e.factory(); + } + + public static ActionFieldSchema schema(String typeId) { + return typeId == null ? null : RESOLVED_SCHEMAS.get(typeId); + } + + public static boolean isCondition(String typeId) { + MacroActionEntry e = typeId == null ? null : ENTRIES.get(typeId); + return e != null && e.isCondition(); + } + + public static String ownerOf(String typeId) { + return typeId == null ? null : ENTRY_OWNERS.get(typeId); + } + + public static List entries() { + return Collections.unmodifiableList(new ArrayList<>(ENTRIES.values())); + } + + private static boolean isBuiltinName(String typeId) { + try { + MacroActionType.valueOf(typeId.toUpperCase(Locale.ROOT)); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static String errorName(Throwable t) { + if (t == null) return "unknown"; + String message = t.getMessage(); + return message == null || message.isBlank() ? t.getClass().getSimpleName() : t.getClass().getSimpleName() + ": " + message; + } +} diff --git a/src/main/java/autismclient/api/macro/MacroExecutionContext.java b/src/main/java/autismclient/api/macro/MacroExecutionContext.java new file mode 100644 index 0000000..375a8f9 --- /dev/null +++ b/src/main/java/autismclient/api/macro/MacroExecutionContext.java @@ -0,0 +1,28 @@ +package autismclient.api.macro; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.Packet; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public interface MacroExecutionContext { + + Minecraft mc(); + + void runOnClientThread(Runnable r); + + T callOnClientThread(Supplier s); + + void waitTicks(int ticks); + + void awaitCondition(CompletableFuture future); + + void awaitCondition(AddonCondition condition); + + boolean isActive(); + + void setStatus(String status); + + void sendPacket(Packet packet); +} diff --git a/src/main/java/autismclient/api/macro/MacroPresetRegistry.java b/src/main/java/autismclient/api/macro/MacroPresetRegistry.java new file mode 100644 index 0000000..216e783 --- /dev/null +++ b/src/main/java/autismclient/api/macro/MacroPresetRegistry.java @@ -0,0 +1,115 @@ +package autismclient.api.macro; + +import autismclient.AutismClientAddon; +import autismclient.api.AddonRegistrationResult; +import autismclient.addons.AddonManager; +import autismclient.util.macro.MacroAction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public final class MacroPresetRegistry { + + public record PresetEntry(String categoryId, String label, String tip, Supplier> builder) {} + + private static final List ENTRIES = new ArrayList<>(); + private static final Map ENTRY_OWNERS = new IdentityHashMap<>(); + private static final Map CATEGORY_LABELS = new LinkedHashMap<>(); + private static final Map CATEGORY_OWNERS = new LinkedHashMap<>(); + + private MacroPresetRegistry() {} + + public static void registerCategory(String id, String label) { + registerCategory(id, label, AddonManager.currentAddonId()); + } + + public static String ensureScopedCategory(String rawId) { + String scopedId = MacroActionRegistry.scopeCategoryId(rawId); + if (!CATEGORY_LABELS.containsKey(scopedId)) { + registerCategory(scopedId, AddonManager.scopedCategoryLabel(null), AddonManager.currentAddonId()); + } + return scopedId; + } + + public static void registerScopedCategory(String rawId, String label) { + String visible = AddonManager.isLifecycleActive() ? AddonManager.scopedCategoryLabel(null) : label; + registerCategory(MacroActionRegistry.scopeCategoryId(rawId), visible, + AddonManager.currentAddonId()); + } + + private static void registerCategory(String id, String label, String ownerAddonId) { + if (id == null || id.isBlank()) return; + CATEGORY_LABELS.put(id, label == null || label.isBlank() ? id : label); + if (ownerAddonId != null && !ownerAddonId.isBlank()) CATEGORY_OWNERS.put(id, ownerAddonId); + } + + public static void register(String categoryId, String label, String tip, Supplier> builder) { + registerDetailed(categoryId, label, tip, builder); + } + + public static AddonRegistrationResult registerDetailed(String categoryId, String label, String tip, Supplier> builder) { + String ownerAddonId = AddonManager.currentAddonId(); + if (ownerAddonId == null || ownerAddonId.isBlank()) { + return reject(ownerAddonId, label, "registration outside an addon lifecycle"); + } + if (categoryId == null || categoryId.isBlank()) return reject(ownerAddonId, label, "missing category"); + if (label == null || label.isBlank()) return reject(ownerAddonId, label, "missing label"); + if (builder == null) return reject(ownerAddonId, label, "missing builder"); + AddonRegistrationResult validation = validateBuilder(ownerAddonId, label, builder); + if (!validation.accepted()) return validation; + PresetEntry entry = new PresetEntry(categoryId, label, tip == null ? "" : tip, builder); + ENTRIES.add(entry); + ENTRY_OWNERS.put(entry, ownerAddonId); + AddonManager.recordAcceptedRegistration("preset", label); + return AddonRegistrationResult.accepted("preset", label); + } + + private static AddonRegistrationResult validateBuilder(String ownerAddonId, String label, Supplier> builder) { + List actions; + try { + actions = builder.get(); + } catch (Throwable t) { + AutismClientAddon.LOG.warn("[MacroPresets] Rejecting preset '{}': builder threw", label, t); + AddonManager.recordRejectedRegistration(ownerAddonId, "preset", label, "builder threw " + t.getClass().getSimpleName()); + return AddonRegistrationResult.rejected("preset", label, "builder threw " + t.getClass().getSimpleName()); + } + if (actions == null) return reject(ownerAddonId, label, "builder returned null"); + for (int i = 0; i < actions.size(); i++) { + if (actions.get(i) == null) { + return reject(ownerAddonId, label, "builder returned null action at index " + i); + } + } + return AddonRegistrationResult.accepted("preset", label); + } + + private static AddonRegistrationResult reject(String ownerAddonId, String label, String reason) { + AutismClientAddon.LOG.warn("[MacroPresets] Rejecting preset '{}': {}", label, reason); + AddonManager.recordRejectedRegistration(ownerAddonId, "preset", label, reason); + return AddonRegistrationResult.rejected("preset", label, reason); + } + + public static void unregisterAddon(String addonId) { + if (addonId == null || addonId.isBlank()) return; + List remove = new ArrayList<>(); + for (PresetEntry entry : ENTRIES) { + if (addonId.equals(ENTRY_OWNERS.get(entry))) remove.add(entry); + } + ENTRIES.removeAll(remove); + for (PresetEntry entry : remove) ENTRY_OWNERS.remove(entry); + CATEGORY_LABELS.entrySet().removeIf(entry -> addonId.equals(CATEGORY_OWNERS.get(entry.getKey()))); + CATEGORY_OWNERS.entrySet().removeIf(entry -> addonId.equals(entry.getValue())); + } + + public static List entries() { + return Collections.unmodifiableList(ENTRIES); + } + + public static String categoryLabel(String id) { + return CATEGORY_LABELS.get(id); + } +} diff --git a/src/main/java/autismclient/api/macro/SimpleAction.java b/src/main/java/autismclient/api/macro/SimpleAction.java new file mode 100644 index 0000000..7775fcb --- /dev/null +++ b/src/main/java/autismclient/api/macro/SimpleAction.java @@ -0,0 +1,33 @@ +package autismclient.api.macro; + +import net.minecraft.client.Minecraft; + +import java.util.function.Consumer; + +public final class SimpleAction extends AddonAction { + private final String displayName; + private final String icon; + private final Consumer runner; + + public SimpleAction(String typeId, String displayName, String icon, Consumer runner) { + super(typeId); + this.displayName = displayName == null || displayName.isBlank() ? typeId : displayName; + this.icon = icon == null || icon.isBlank() ? "*" : icon; + this.runner = runner; + } + + @Override + protected void run(Minecraft mc) { + if (runner != null) runner.accept(mc); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public String getIcon() { + return icon; + } +} diff --git a/src/main/java/autismclient/api/macro/SimpleCondition.java b/src/main/java/autismclient/api/macro/SimpleCondition.java new file mode 100644 index 0000000..eaa6ea5 --- /dev/null +++ b/src/main/java/autismclient/api/macro/SimpleCondition.java @@ -0,0 +1,41 @@ +package autismclient.api.macro; + +import net.minecraft.client.Minecraft; + +import java.util.function.Predicate; + +public final class SimpleCondition extends AddonContextAction { + private final String displayName; + private final String status; + private final String icon; + private final Predicate predicate; + + public SimpleCondition(String typeId, String displayName, String status, String icon, Predicate predicate) { + super(typeId); + this.displayName = displayName == null || displayName.isBlank() ? typeId : displayName; + this.status = status == null || status.isBlank() ? this.displayName : status; + this.icon = icon == null || icon.isBlank() ? "?" : icon; + this.predicate = predicate; + } + + @Override + public void run(MacroExecutionContext ctx) { + ctx.setStatus(status); + ctx.awaitCondition(new AddonCondition() { + @Override + public boolean check(Minecraft mc) { + return predicate != null && predicate.test(mc); + } + }); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public String getIcon() { + return icon; + } +} diff --git a/src/main/java/autismclient/api/module/SimpleModule.java b/src/main/java/autismclient/api/module/SimpleModule.java new file mode 100644 index 0000000..91099b7 --- /dev/null +++ b/src/main/java/autismclient/api/module/SimpleModule.java @@ -0,0 +1,102 @@ +package autismclient.api.module; + +import autismclient.api.AutismAddons; +import autismclient.modules.PackModule; +import autismclient.modules.PackModuleOption; + +import java.util.Locale; +import java.util.function.Consumer; + +public class SimpleModule extends PackModule { + private Consumer enableHandler; + private Consumer disableHandler; + private Consumer tickHandler; + + public SimpleModule(String localId, String name, String description) { + super(AutismAddons.id(localId), name, description); + } + + public final SimpleModule onEnabled(Consumer handler) { + this.enableHandler = handler; + return this; + } + + public final SimpleModule onDisabled(Consumer handler) { + this.disableHandler = handler; + return this; + } + + public final SimpleModule onTick(Consumer handler) { + this.tickHandler = handler; + return this; + } + + public final SimpleModule addBool(String id, String label, boolean defaultValue) { + option(PackModuleOption.bool(id, label, defaultValue)); + return this; + } + + public final SimpleModule addInt(String id, String label, int defaultValue, int min, int max, int step) { + option(PackModuleOption.integer(id, label, defaultValue, min, max, step)); + return this; + } + + public final SimpleModule addDouble(String id, String label, double defaultValue, double min, double max, double step) { + option(PackModuleOption.decimal(id, label, defaultValue, min, max, step)); + return this; + } + + public final SimpleModule addChoice(String id, String label, String defaultValue, String... choices) { + option(PackModuleOption.enumChoice(id, label, defaultValue, choices)); + return this; + } + + public final SimpleModule addText(String id, String label, String defaultValue) { + option(PackModuleOption.text(id, label, defaultValue)); + return this; + } + + public final SimpleModule addColor(String id, String label, int defaultArgb) { + option(PackModuleOption.color(id, label, defaultArgb)); + return this; + } + + public final boolean getBool(String id) { + return bool(id); + } + + public final int getInt(String id) { + return integer(id); + } + + public final double getDouble(String id) { + return decimal(id); + } + + public final String getText(String id) { + return text(id); + } + + public final String getChoice(String id) { + return choice(id); + } + + @Override + public void onEnable() { + if (enableHandler != null) enableHandler.accept(this); + } + + @Override + public void onDisable() { + if (disableHandler != null) disableHandler.accept(this); + } + + @Override + public void tick() { + if (tickHandler != null) tickHandler.accept(this); + } + + protected final String upper(String value) { + return value == null ? "" : value.toUpperCase(Locale.ROOT); + } +} diff --git a/src/main/java/autismclient/commands/AutismCommandSource.java b/src/main/java/autismclient/commands/AutismCommandSource.java new file mode 100644 index 0000000..13c0cce --- /dev/null +++ b/src/main/java/autismclient/commands/AutismCommandSource.java @@ -0,0 +1,18 @@ +package autismclient.commands; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.multiplayer.ClientPacketListener; + +public final class AutismCommandSource { + public static final AutismCommandSource INSTANCE = new AutismCommandSource(); + + private AutismCommandSource() {} + + public Minecraft mc() { return Minecraft.getInstance(); } + public LocalPlayer player() { return Minecraft.getInstance().player; } + public ClientPacketListener connection() { return Minecraft.getInstance().getConnection(); } + + public boolean hasPlayer() { return Minecraft.getInstance().player != null; } + public boolean hasConnection() { return Minecraft.getInstance().getConnection() != null; } +} diff --git a/src/main/java/autismclient/commands/AutismCommands.java b/src/main/java/autismclient/commands/AutismCommands.java new file mode 100644 index 0000000..a76788d --- /dev/null +++ b/src/main/java/autismclient/commands/AutismCommands.java @@ -0,0 +1,222 @@ +package autismclient.commands; + +import autismclient.AutismClientAddon; +import autismclient.api.AddonRegistrationResult; +import autismclient.modules.PackHideState; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismCompatManager; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.util.Set; + +public final class AutismCommands { + private static final CommandDispatcher DISPATCHER = new CommandDispatcher<>(); + private static final List ALL = new ArrayList<>(); + private static final Map BY_NAME = new LinkedHashMap<>(); + private static final Map ADDON_COMMAND_OWNERS = new LinkedHashMap<>(); + private static final Map ADDON_COMMAND_OBJECT_OWNERS = new IdentityHashMap<>(); + private static final Set DISABLED_ADDON_COMMAND_NAMES = new HashSet<>(); + private static boolean initialized = false; + + private AutismCommands() {} + + public static synchronized void init() { + if (initialized) return; + initialized = true; + register(new autismclient.commands.impl.ToggleCommand()); + register(new autismclient.commands.impl.MacroCommand()); + register(new autismclient.commands.impl.DelayCommand()); + register(new autismclient.commands.impl.SendCommand()); + register(new autismclient.commands.impl.VClipCommand()); + register(new autismclient.commands.impl.HClipCommand()); + register(new autismclient.commands.impl.NbtCommand()); + register(new autismclient.commands.impl.ServerCommand()); + register(new autismclient.commands.impl.PluginsCommand()); + register(new autismclient.commands.impl.PrefixCommand()); + register(new autismclient.commands.impl.CommandsCommand()); + register(new autismclient.commands.impl.HelpCommand()); + register(new autismclient.commands.impl.ModulesCommand()); + register(new autismclient.commands.impl.BindsCommand()); + register(new autismclient.commands.impl.BindCommand()); + register(new autismclient.commands.impl.DismountCommand()); + register(new autismclient.commands.impl.DisconnectCommand()); + register(new autismclient.commands.impl.SayCommand()); + register(new autismclient.commands.impl.DropCommand()); + register(new autismclient.commands.impl.GamemodeCommand()); + register(new autismclient.commands.impl.GiveCommand()); + register(new autismclient.commands.impl.DamageCommand()); + register(new autismclient.commands.impl.XCarryCommand()); + } + + private static void register(Command command) { + registerWithName(command, command.name(), true); + } + + private static List registerWithName(Command command, String primaryName, boolean registerAliases) { + List registeredNames = new ArrayList<>(); + ALL.add(command); + LiteralArgumentBuilder primary = LiteralArgumentBuilder.literal(primaryName); + command.build(primary); + DISPATCHER.register(primary); + BY_NAME.put(primaryName.toLowerCase(Locale.ROOT), command); + DISABLED_ADDON_COMMAND_NAMES.remove(primaryName.toLowerCase(Locale.ROOT)); + registeredNames.add(primaryName.toLowerCase(Locale.ROOT)); + + if (!registerAliases) return registeredNames; + for (String alias : command.aliases()) { + if (alias == null || alias.isBlank()) continue; + if (BY_NAME.containsKey(alias.toLowerCase(Locale.ROOT))) continue; + LiteralArgumentBuilder aliasBuilder = LiteralArgumentBuilder.literal(alias); + command.build(aliasBuilder); + DISPATCHER.register(aliasBuilder); + BY_NAME.put(alias.toLowerCase(Locale.ROOT), command); + DISABLED_ADDON_COMMAND_NAMES.remove(alias.toLowerCase(Locale.ROOT)); + registeredNames.add(alias.toLowerCase(Locale.ROOT)); + } + return registeredNames; + } + + public static synchronized void registerAddonCommand(Command command, String addonId) { + registerAddonCommandDetailed(command, addonId); + } + + public static synchronized AddonRegistrationResult registerAddonCommandDetailed(Command command, String addonId) { + if (command == null) return AddonRegistrationResult.rejected("command", "", "command was null"); + if (addonId == null || addonId.isBlank()) { + return rejectAddonCommand(addonId, "", "registration outside an addon lifecycle"); + } + String name = command.name(); + if (name == null || name.isBlank()) { + return rejectAddonCommand(addonId, "", "blank command name"); + } + List registered; + boolean collision = BY_NAME.containsKey(name.toLowerCase(Locale.ROOT)); + if (collision) { + String namespaced = (addonId == null || addonId.isBlank() ? "addon" : addonId) + ":" + name; + AutismClientAddon.LOG.warn("[Commands] Command name '{}' from addon '{}' collides with an existing " + + "command; registering it as '{}' instead", name, addonId, namespaced); + registered = registerWithName(command, namespaced, false); + } else { + registered = registerWithName(command, name, true); + } + ADDON_COMMAND_OBJECT_OWNERS.put(command, addonId); + for (String registeredName : registered) { + ADDON_COMMAND_OWNERS.put(registeredName, addonId); + } + String id = registered.isEmpty() ? name : registered.get(0); + autismclient.addons.AddonManager.recordAcceptedRegistration("command", id); + return AddonRegistrationResult.accepted("command", id); + } + + private static AddonRegistrationResult rejectAddonCommand(String addonId, String id, String reason) { + AutismClientAddon.LOG.warn("[Commands] Rejecting addon command '{}': {}", id, reason); + autismclient.addons.AddonManager.recordRejectedRegistration(addonId, "command", id, reason); + return AddonRegistrationResult.rejected("command", id, reason); + } + + public static synchronized void unregisterAddonCommands(String addonId) { + if (addonId == null || addonId.isBlank()) return; + List removeNames = new ArrayList<>(); + for (Map.Entry owner : ADDON_COMMAND_OWNERS.entrySet()) { + if (addonId.equals(owner.getValue())) removeNames.add(owner.getKey()); + } + for (String name : removeNames) { + BY_NAME.remove(name); + ADDON_COMMAND_OWNERS.remove(name); + DISABLED_ADDON_COMMAND_NAMES.add(name); + } + ALL.removeIf(command -> addonId.equals(ADDON_COMMAND_OBJECT_OWNERS.get(command))); + ADDON_COMMAND_OBJECT_OWNERS.entrySet().removeIf(entry -> addonId.equals(entry.getValue())); + } + + public static CommandDispatcher dispatcher() { return DISPATCHER; } + + public static List all() { return Collections.unmodifiableList(ALL); } + + public static Command find(String nameOrAlias) { + if (nameOrAlias == null) return null; + return BY_NAME.get(nameOrAlias.trim().toLowerCase(Locale.ROOT)); + } + + public static String effectivePrefix() { return AutismCompatManager.effectiveCommandPrefix(); } + + public static boolean isAutismCommandMessage(String message) { + if (commandsBlockedByPanic()) return false; + if (message == null || message.isBlank()) return false; + String trimmed = message.trim(); + String prefix = effectivePrefix(); + return !prefix.isEmpty() && trimmed.startsWith(prefix); + } + + public static boolean commandsBlockedByPanic() { + return PackHideState.isActive(); + } + + public static boolean isBlockedPanicCommandMessage(String message) { + return commandsBlockedByPanic() && isAutismCommandMessage(message); + } + + public static String commandBody(String message) { + if (!isAutismCommandMessage(message)) return ""; + String trimmed = message.trim(); + int prefixLength = effectivePrefix().length(); + return trimmed.length() <= prefixLength ? "" : trimmed.substring(prefixLength).trim(); + } + + public static boolean dispatch(String body) { + if (commandsBlockedByPanic()) return true; + if (body == null) return false; + String trimmed = body.trim(); + if (trimmed.isEmpty()) return false; + if (DISABLED_ADDON_COMMAND_NAMES.contains(firstToken(trimmed).toLowerCase(Locale.ROOT))) { + sendSyntaxError(trimmed, CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().create()); + return true; + } + try { + DISPATCHER.execute(trimmed, AutismCommandSource.INSTANCE); + return true; + } catch (CommandSyntaxException e) { + sendSyntaxError(trimmed, e); + return true; + } catch (Exception e) { + AutismClientAddon.LOG.warn("[Commands] dispatch failed for '{}'", trimmed, e); + AutismClientMessaging.sendPrefixed("§cCommand error: " + + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); + return true; + } + } + + private static void sendSyntaxError(String body, CommandSyntaxException e) { + String first = firstToken(body); + Command command = find(first); + String prefix = effectivePrefix(); + if (command == null) { + AutismClientMessaging.sendPrefixed("§cUnknown AUTISM command: §f" + first); + AutismClientMessaging.sendPrefixed("§7Use §f" + prefix + "commands §7or §f" + prefix + "help§7."); + return; + } + + String message = e.getMessage(); + if (message == null || message.isBlank()) message = "Incomplete or invalid command."; + AutismClientMessaging.sendPrefixed("§c" + message); + AutismClientMessaging.sendPrefixed("§7Use §f" + prefix + "help " + command.name() + "§7."); + } + + private static String firstToken(String body) { + if (body == null) return ""; + String trimmed = body.trim(); + if (trimmed.isEmpty()) return ""; + int space = trimmed.indexOf(' '); + return space < 0 ? trimmed : trimmed.substring(0, space); + } +} diff --git a/src/main/java/autismclient/commands/Command.java b/src/main/java/autismclient/commands/Command.java new file mode 100644 index 0000000..f643705 --- /dev/null +++ b/src/main/java/autismclient/commands/Command.java @@ -0,0 +1,33 @@ +package autismclient.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; + +public abstract class Command { + public static final int SUCCESS = 1; + + private final String name; + private final String description; + private final String[] aliases; + + protected Command(String name, String description, String... aliases) { + this.name = name; + this.description = description == null ? "" : description; + this.aliases = aliases == null ? new String[0] : aliases.clone(); + } + + public final String name() { return name; } + public final String description() { return description; } + public final String[] aliases() { return aliases.clone(); } + + public abstract void build(LiteralArgumentBuilder builder); + + public int run(CommandContext ctx) { + autismclient.util.AutismClientMessaging.sendPrefixed( + "§eUsage: " + AutismCommands.effectivePrefix() + name + (aliases.length > 0 ? " (alias: " + aliases[0] + ")" : "")); + if (!description.isEmpty()) { + autismclient.util.AutismClientMessaging.sendPrefixed("§7" + description); + } + return SUCCESS; + } +} diff --git a/src/main/java/autismclient/commands/CommandSuggest.java b/src/main/java/autismclient/commands/CommandSuggest.java new file mode 100644 index 0000000..8511042 --- /dev/null +++ b/src/main/java/autismclient/commands/CommandSuggest.java @@ -0,0 +1,65 @@ +package autismclient.commands; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public final class CommandSuggest { + private CommandSuggest() {} + + public static CompletableFuture literals(SuggestionsBuilder builder, String... values) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (String value : values) { + if (value != null && value.toLowerCase(Locale.ROOT).startsWith(remaining)) builder.suggest(value); + } + return builder.buildFuture(); + } + + public static CompletableFuture state(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "on", "off", "toggle", "enable", "disable"); + } + + public static CompletableFuture offsets(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "1", "5", "10", "50", "100", "-1", "-5", "-10", "-50", "-100"); + } + + public static CompletableFuture vclipSegments(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "1", "5", "10", "20", "50"); + } + + public static CompletableFuture vclipPacketLimits(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "1", "5", "10", "20", "40", "100"); + } + + public static CompletableFuture damage(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "1", "2", "4", "8", "20"); + } + + public static CompletableFuture counts(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "1", "16", "32", "64"); + } + + public static CompletableFuture gamemodes(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, "survival", "creative", "adventure", "spectator", "reset", "0", "1", "2", "3"); + } + + public static CompletableFuture prefixes(CommandContext ignored, SuggestionsBuilder builder) { + return literals(builder, ".", "%", "-", "_", "*", "#", "@", "&", "="); + } + + public static CompletableFuture itemIds(CommandContext ignored, SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (Identifier id : BuiltInRegistries.ITEM.keySet()) { + String full = id.toString(); + String shortId = id.getNamespace().equals("minecraft") ? id.getPath() : full; + if (full.toLowerCase(Locale.ROOT).startsWith(remaining)) builder.suggest(full); + if (!shortId.equals(full) && shortId.toLowerCase(Locale.ROOT).startsWith(remaining)) builder.suggest(shortId); + } + return builder.buildFuture(); + } +} diff --git a/src/main/java/autismclient/commands/args/KeyArgumentType.java b/src/main/java/autismclient/commands/args/KeyArgumentType.java new file mode 100644 index 0000000..71854c1 --- /dev/null +++ b/src/main/java/autismclient/commands/args/KeyArgumentType.java @@ -0,0 +1,72 @@ +package autismclient.commands.args; + +import autismclient.commands.AutismCommandSource; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; + +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class KeyArgumentType implements ArgumentType { + private static final Map NAMES = new LinkedHashMap<>(); + private static final SimpleCommandExceptionType UNKNOWN = new SimpleCommandExceptionType(Component.literal("Unknown key")); + + static { + for (Field f : GLFW.class.getDeclaredFields()) { + String n = f.getName(); + if (!n.startsWith("GLFW_KEY_")) continue; + try { + int code = f.getInt(null); + NAMES.put(n.substring("GLFW_KEY_".length()).toUpperCase(Locale.ROOT), code); + } catch (Throwable ignored) {} + } + } + + public static KeyArgumentType key() { return new KeyArgumentType(); } + + public static int get(CommandContext ctx, String name) { + return ctx.getArgument(name, Integer.class); + } + + public static String keyName(int code) { + if (code < 0) return "NONE"; + for (Map.Entry e : NAMES.entrySet()) { + if (e.getValue() == code) return e.getKey(); + } + return "KEY_" + code; + } + + @Override + public Integer parse(StringReader reader) throws CommandSyntaxException { + String raw = reader.readUnquotedString(); + String upper = raw.toUpperCase(Locale.ROOT); + Integer code = NAMES.get(upper); + if (code != null) return code; + + if (upper.length() == 1) { + char c = upper.charAt(0); + if (c >= 'A' && c <= 'Z') return GLFW.GLFW_KEY_A + (c - 'A'); + if (c >= '0' && c <= '9') return GLFW.GLFW_KEY_0 + (c - '0'); + } + throw UNKNOWN.create(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toUpperCase(Locale.ROOT); + for (String n : NAMES.keySet()) { + if (n.startsWith(remaining)) builder.suggest(n); + } + return builder.buildFuture(); + } +} diff --git a/src/main/java/autismclient/commands/args/MacroArgumentType.java b/src/main/java/autismclient/commands/args/MacroArgumentType.java new file mode 100644 index 0000000..913b75d --- /dev/null +++ b/src/main/java/autismclient/commands/args/MacroArgumentType.java @@ -0,0 +1,41 @@ +package autismclient.commands.args; + +import autismclient.commands.AutismCommandSource; +import autismclient.util.AutismMacroManager; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public class MacroArgumentType implements ArgumentType { + public static MacroArgumentType macroName() { return new MacroArgumentType(); } + + public static String get(CommandContext ctx, String name) { + return ctx.getArgument(name, String.class); + } + + @Override + public String parse(StringReader reader) throws CommandSyntaxException { + + if (reader.canRead() && reader.peek() == '"') return reader.readQuotedString(); + return reader.readUnquotedString(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (autismclient.util.AutismMacro macro : AutismMacroManager.get().getAll()) { + if (macro == null || macro.name == null) continue; + String n = macro.name; + if (n.toLowerCase(Locale.ROOT).startsWith(remaining)) { + builder.suggest(n.contains(" ") ? "\"" + n + "\"" : n); + } + } + return builder.buildFuture(); + } +} diff --git a/src/main/java/autismclient/commands/args/ModuleArgumentType.java b/src/main/java/autismclient/commands/args/ModuleArgumentType.java new file mode 100644 index 0000000..94d0c23 --- /dev/null +++ b/src/main/java/autismclient/commands/args/ModuleArgumentType.java @@ -0,0 +1,41 @@ +package autismclient.commands.args; + +import autismclient.commands.AutismCommandSource; +import autismclient.modules.PackModuleRegistry; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public class ModuleArgumentType implements ArgumentType { + public static ModuleArgumentType moduleName() { return new ModuleArgumentType(); } + + public static String get(CommandContext ctx, String name) { + return ctx.getArgument(name, String.class); + } + + @Override + public String parse(StringReader reader) throws CommandSyntaxException { + return reader.readUnquotedString(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (String name : PackModuleRegistry.names()) { + if (name == null) continue; + if (name.toLowerCase(Locale.ROOT).startsWith(remaining)) { + builder.suggest(name); + } + } + + if ("all".startsWith(remaining)) builder.suggest("all"); + if ("hud".startsWith(remaining)) builder.suggest("hud"); + return builder.buildFuture(); + } +} diff --git a/src/main/java/autismclient/commands/args/PacketClassArgumentType.java b/src/main/java/autismclient/commands/args/PacketClassArgumentType.java new file mode 100644 index 0000000..3e2d5c6 --- /dev/null +++ b/src/main/java/autismclient/commands/args/PacketClassArgumentType.java @@ -0,0 +1,39 @@ +package autismclient.commands.args; + +import autismclient.commands.AutismCommandSource; +import autismclient.util.AutismPacketRegistry; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.network.protocol.Packet; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public class PacketClassArgumentType implements ArgumentType { + public static PacketClassArgumentType packetClass() { return new PacketClassArgumentType(); } + + public static String get(CommandContext ctx, String name) { + return ctx.getArgument(name, String.class); + } + + @Override + public String parse(StringReader reader) throws CommandSyntaxException { + return reader.readUnquotedString(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + + for (Class> cls : AutismPacketRegistry.getC2SPackets()) { + String name = AutismPacketRegistry.getName(cls); + if (name == null) continue; + if (name.toLowerCase(Locale.ROOT).contains(remaining)) builder.suggest(name); + } + return builder.buildFuture(); + } +} diff --git a/src/main/java/autismclient/commands/args/XCarrySlotArgumentType.java b/src/main/java/autismclient/commands/args/XCarrySlotArgumentType.java new file mode 100644 index 0000000..b3fb269 --- /dev/null +++ b/src/main/java/autismclient/commands/args/XCarrySlotArgumentType.java @@ -0,0 +1,65 @@ +package autismclient.commands.args; + +import autismclient.commands.AutismCommandSource; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.network.chat.Component; + +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class XCarrySlotArgumentType implements ArgumentType { + public static final int CURSOR = Integer.MIN_VALUE; + + private static final Map TOKENS = new LinkedHashMap<>(); + static { + TOKENS.put("craft1", 1); + TOKENS.put("craft2", 2); + TOKENS.put("craft3", 3); + TOKENS.put("craft4", 4); + TOKENS.put("helmet", 5); + TOKENS.put("chestplate", 6); + TOKENS.put("leggings", 7); + TOKENS.put("boots", 8); + TOKENS.put("offhand", 45); + TOKENS.put("cursor", CURSOR); + } + + private static final SimpleCommandExceptionType UNKNOWN = + new SimpleCommandExceptionType(Component.literal("Unknown XCarry slot — use craft1..craft4 / helmet / chestplate / leggings / boots / offhand / cursor.")); + + public static XCarrySlotArgumentType slot() { return new XCarrySlotArgumentType(); } + + public static int get(CommandContext ctx, String name) { + return ctx.getArgument(name, Integer.class); + } + + public static String displayName(int slotId) { + for (Map.Entry e : TOKENS.entrySet()) if (e.getValue() == slotId) return e.getKey(); + return "slot " + slotId; + } + + @Override + public Integer parse(StringReader reader) throws CommandSyntaxException { + String raw = reader.readUnquotedString().toLowerCase(Locale.ROOT); + Integer id = TOKENS.get(raw); + if (id == null) throw UNKNOWN.create(); + return id; + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (String name : TOKENS.keySet()) { + if (name.startsWith(remaining)) builder.suggest(name); + } + return builder.buildFuture(); + } +} diff --git a/src/main/java/autismclient/commands/impl/BindCommand.java b/src/main/java/autismclient/commands/impl/BindCommand.java new file mode 100644 index 0000000..6a1423d --- /dev/null +++ b/src/main/java/autismclient/commands/impl/BindCommand.java @@ -0,0 +1,42 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.args.KeyArgumentType; +import autismclient.modules.AutismModule; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; + +public class BindCommand extends Command { + public BindCommand() { super("bind", "Bind a key to a command. Usage: bind | bind clear "); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("§eUsage: §f" + prefix + "bind "); + AutismClientMessaging.sendPrefixed("§7Example: §f" + prefix + "bind G " + prefix + "macro myMacro"); + return SUCCESS; + }); + root.then(LiteralArgumentBuilder.literal("clear") + .then(RequiredArgumentBuilder.argument("key", KeyArgumentType.key()) + .executes(ctx -> { + int key = KeyArgumentType.get(ctx, "key"); + AutismModule.get().clearCommandBind(key); + AutismClientMessaging.sendPrefixed("§eCleared bind for §f" + KeyArgumentType.keyName(key)); + return SUCCESS; + }))); + root.then(RequiredArgumentBuilder.argument("key", KeyArgumentType.key()) + .then(RequiredArgumentBuilder.argument("command", StringArgumentType.greedyString()) + .executes(ctx -> { + int key = KeyArgumentType.get(ctx, "key"); + String cmd = StringArgumentType.getString(ctx, "command"); + AutismModule.get().setCommandBind(key, cmd); + AutismClientMessaging.sendPrefixed("§aBound §f" + KeyArgumentType.keyName(key) + "§a → §f" + cmd); + return SUCCESS; + }))); + } +} diff --git a/src/main/java/autismclient/commands/impl/BindsCommand.java b/src/main/java/autismclient/commands/impl/BindsCommand.java new file mode 100644 index 0000000..4c14cf3 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/BindsCommand.java @@ -0,0 +1,27 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.args.KeyArgumentType; +import autismclient.modules.AutismModule; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; + +import java.util.Map; + +public class BindsCommand extends Command { + public BindsCommand() { super("binds", "List command keybinds (set with `bind`)."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + Map binds = AutismModule.get().getCommandBinds(); + if (binds.isEmpty()) { AutismClientMessaging.sendPrefixed("§eNo command keybinds set."); return SUCCESS; } + AutismClientMessaging.sendPrefixed("§e" + binds.size() + " command keybinds:"); + for (Map.Entry e : binds.entrySet()) { + AutismClientMessaging.sendPrefixed("§b" + KeyArgumentType.keyName(e.getKey()) + "§7 → §f" + e.getValue()); + } + return SUCCESS; + }); + } +} diff --git a/src/main/java/autismclient/commands/impl/CommandsCommand.java b/src/main/java/autismclient/commands/impl/CommandsCommand.java new file mode 100644 index 0000000..c5f52cc --- /dev/null +++ b/src/main/java/autismclient/commands/impl/CommandsCommand.java @@ -0,0 +1,28 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; + +public class CommandsCommand extends Command { + public CommandsCommand() { super("commands", "List all available commands.", "cmds"); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("§e" + AutismCommands.all().size() + " commands (prefix §f" + prefix + "§e):"); + StringBuilder line = new StringBuilder(); + for (Command c : AutismCommands.all()) { + if (line.length() > 0) line.append("§7, "); + line.append("§f").append(c.name()); + if (line.length() > 200) { AutismClientMessaging.sendPrefixed(line.toString()); line.setLength(0); } + } + if (line.length() > 0) AutismClientMessaging.sendPrefixed(line.toString()); + AutismClientMessaging.sendPrefixed("§7Type §f" + prefix + "help §7 for details."); + return SUCCESS; + }); + } +} diff --git a/src/main/java/autismclient/commands/impl/DamageCommand.java b/src/main/java/autismclient/commands/impl/DamageCommand.java new file mode 100644 index 0000000..ebe17fa --- /dev/null +++ b/src/main/java/autismclient/commands/impl/DamageCommand.java @@ -0,0 +1,32 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.CommandSuggest; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; + +public class DamageCommand extends Command { + public DamageCommand() { super("damage", "Take damage to self (vanilla server allows via /damage if op)."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("§eUsage: " + AutismCommands.effectivePrefix() + "damage "); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("amount", DoubleArgumentType.doubleArg(0.1, 1024.0)) + .suggests(CommandSuggest::damage) + .executes(ctx -> { + double amt = DoubleArgumentType.getDouble(ctx, "amount"); + var conn = Minecraft.getInstance().getConnection(); + if (conn == null) { AutismClientMessaging.sendPrefixed("§cNot connected."); return SUCCESS; } + conn.sendCommand("damage @s " + amt); + return SUCCESS; + })); + } +} diff --git a/src/main/java/autismclient/commands/impl/DelayCommand.java b/src/main/java/autismclient/commands/impl/DelayCommand.java new file mode 100644 index 0000000..ffd22f7 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/DelayCommand.java @@ -0,0 +1,50 @@ +package autismclient.commands.impl; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.Command; +import autismclient.commands.CommandSuggest; +import autismclient.modules.AutismModule; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismSharedState; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; + +public class DelayCommand extends Command { + public DelayCommand() { super("delay", "Toggle the GUI packet delay queue."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> apply(null)); + root.then(RequiredArgumentBuilder.argument("state", StringArgumentType.word()) + .suggests(CommandSuggest::state) + .executes(ctx -> apply(StringArgumentType.getString(ctx, "state")))); + } + + private static int apply(String state) { + boolean current = AutismSharedState.get().shouldDelayGuiPackets(); + if (state != null) { + String lower = state.toLowerCase(); + if (!lower.equals("on") && !lower.equals("enable") && !lower.equals("true") && !lower.equals("1") + && !lower.equals("off") && !lower.equals("disable") && !lower.equals("false") && !lower.equals("0") + && !lower.equals("toggle")) { + AutismClientMessaging.sendPrefixed("Unknown delay state: " + state); + AutismClientMessaging.sendPrefixed("Use on, off, or toggle."); + return SUCCESS; + } + } + + boolean target = state == null ? !current + : switch (state.toLowerCase()) { + case "on", "enable", "true", "1" -> true; + case "off", "disable", "false", "0" -> false; + case "toggle" -> !current; + default -> !current; + }; + + AutismModule module = AutismModule.get(); + int flushed = module.applyDelayGuiPacketsUiBehavior(target); + module.notifyDelayPacketsUiResult(target, flushed); + return SUCCESS; + } +} diff --git a/src/main/java/autismclient/commands/impl/DisconnectCommand.java b/src/main/java/autismclient/commands/impl/DisconnectCommand.java new file mode 100644 index 0000000..b53052b --- /dev/null +++ b/src/main/java/autismclient/commands/impl/DisconnectCommand.java @@ -0,0 +1,26 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +public class DisconnectCommand extends Command { + public DisconnectCommand() { super("disconnect", "Disconnect from the current server."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + Minecraft mc = Minecraft.getInstance(); + if (mc.getConnection() == null) { AutismClientMessaging.sendPrefixed("§cNot connected."); return SUCCESS; } + try { + mc.getConnection().getConnection().disconnect(Component.literal("Disconnected via .disconnect")); + } catch (Throwable t) { + AutismClientMessaging.sendPrefixed("§cDisconnect failed: " + t.getMessage()); + } + return SUCCESS; + }); + } +} diff --git a/src/main/java/autismclient/commands/impl/DismountCommand.java b/src/main/java/autismclient/commands/impl/DismountCommand.java new file mode 100644 index 0000000..025b6f7 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/DismountCommand.java @@ -0,0 +1,28 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.game.ServerboundPlayerInputPacket; + +public class DismountCommand extends Command { + public DismountCommand() { super("dismount", "Force-dismount from any vehicle."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.getConnection() == null) { AutismClientMessaging.sendPrefixed("§cNot in a world."); return SUCCESS; } + if (mc.player.getVehicle() == null) { AutismClientMessaging.sendPrefixed("§eNot riding anything."); return SUCCESS; } + try { + mc.player.removeVehicle(); + AutismClientMessaging.sendPrefixed("§aDismounted."); + } catch (Throwable t) { + AutismClientMessaging.sendPrefixed("§cDismount failed: " + t.getMessage()); + } + return SUCCESS; + }); + } +} diff --git a/src/main/java/autismclient/commands/impl/DropCommand.java b/src/main/java/autismclient/commands/impl/DropCommand.java new file mode 100644 index 0000000..101e7aa --- /dev/null +++ b/src/main/java/autismclient/commands/impl/DropCommand.java @@ -0,0 +1,58 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; + +public class DropCommand extends Command { + public DropCommand() { super("drop", "Drop hand|hotbar|all from inventory."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { dropHand(); return SUCCESS; }); + root.then(LiteralArgumentBuilder.literal("hand").executes(ctx -> { dropHand(); return SUCCESS; })); + root.then(LiteralArgumentBuilder.literal("hotbar").executes(ctx -> { dropHotbar(); return SUCCESS; })); + root.then(LiteralArgumentBuilder.literal("all").executes(ctx -> { dropAll(); return SUCCESS; })); + } + + private static LocalPlayer player() { return Minecraft.getInstance().player; } + + private static void dropHand() { + LocalPlayer p = player(); + if (p == null) { AutismClientMessaging.sendPrefixed("§cNot in a world."); return; } + p.drop(true); + } + + private static void dropHotbar() { + LocalPlayer p = player(); + if (p == null) { AutismClientMessaging.sendPrefixed("§cNot in a world."); return; } + int original = p.getInventory().getSelectedSlot(); + for (int slot = 0; slot < 9; slot++) { + p.getInventory().setSelectedSlot(slot); + p.drop(true); + } + p.getInventory().setSelectedSlot(original); + } + + private static void dropAll() { + LocalPlayer p = player(); + if (p == null) { AutismClientMessaging.sendPrefixed("§cNot in a world."); return; } + + var conn = Minecraft.getInstance().getConnection(); + if (conn == null) return; + int original = p.getInventory().getSelectedSlot(); + for (int slot = 0; slot < 9; slot++) { + p.getInventory().setSelectedSlot(slot); + conn.send(new ServerboundPlayerActionPacket( + ServerboundPlayerActionPacket.Action.DROP_ALL_ITEMS, BlockPos.ZERO, Direction.DOWN)); + } + p.getInventory().setSelectedSlot(original); + AutismClientMessaging.sendPrefixed("§eDropped hotbar. (Main inventory drop requires open inventory — use macro DropAction for that.)"); + } +} diff --git a/src/main/java/autismclient/commands/impl/GamemodeCommand.java b/src/main/java/autismclient/commands/impl/GamemodeCommand.java new file mode 100644 index 0000000..5ddaaff --- /dev/null +++ b/src/main/java/autismclient/commands/impl/GamemodeCommand.java @@ -0,0 +1,44 @@ +package autismclient.commands.impl; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.Command; +import autismclient.commands.CommandSuggest; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismFakeGamemode; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.world.level.GameType; + +public class GamemodeCommand extends Command { + public GamemodeCommand() { + super("gamemode", "Set fake client-side game mode."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("§eUsage: " + AutismCommands.effectivePrefix() + "gamemode "); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("mode", StringArgumentType.word()) + .suggests(CommandSuggest::gamemodes) + .executes(ctx -> { + String mode = StringArgumentType.getString(ctx, "mode").toLowerCase(java.util.Locale.ROOT); + AutismFakeGamemode.Result result; + if ("reset".equals(mode) || "r".equals(mode)) { + result = AutismFakeGamemode.reset(); + } else { + GameType resolved = AutismFakeGamemode.parseMode(mode); + if (resolved == null) { + AutismClientMessaging.sendPrefixed("§cUnknown mode: §f" + mode); + return SUCCESS; + } + result = AutismFakeGamemode.apply(resolved); + } + AutismClientMessaging.sendPrefixed((result.success() ? "§a" : "§c") + result.message()); + return SUCCESS; + })); + } +} diff --git a/src/main/java/autismclient/commands/impl/GiveCommand.java b/src/main/java/autismclient/commands/impl/GiveCommand.java new file mode 100644 index 0000000..a7b0146 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/GiveCommand.java @@ -0,0 +1,52 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.CommandSuggest; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +public class GiveCommand extends Command { + public GiveCommand() { super("give", "Give item to self (creative only)."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("§eUsage: " + AutismCommands.effectivePrefix() + "give [count]"); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("item", StringArgumentType.word()) + .suggests(CommandSuggest::itemIds) + .executes(ctx -> give(StringArgumentType.getString(ctx, "item"), 1)) + .then(RequiredArgumentBuilder.argument("count", IntegerArgumentType.integer(1, 64)) + .suggests(CommandSuggest::counts) + .executes(ctx -> give(StringArgumentType.getString(ctx, "item"), IntegerArgumentType.getInteger(ctx, "count"))))); + } + + private static int give(String itemId, int count) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { AutismClientMessaging.sendPrefixed("§cNot in a world."); return SUCCESS; } + if (!mc.player.getAbilities().instabuild) { AutismClientMessaging.sendPrefixed("§cCreative mode required."); return SUCCESS; } + Identifier id = itemId.contains(":") ? Identifier.tryParse(itemId) : Identifier.tryParse("minecraft:" + itemId); + if (id == null) { AutismClientMessaging.sendPrefixed("§cInvalid item id: §f" + itemId); return SUCCESS; } + Item item = BuiltInRegistries.ITEM.getOptional(id).orElse(null); + if (item == null) { AutismClientMessaging.sendPrefixed("§cUnknown item: §f" + id); return SUCCESS; } + ItemStack stack = new ItemStack(item, count); + int slot = mc.player.getInventory().getSelectedSlot(); + int containerSlot = 36 + slot; + mc.getConnection().send(new ServerboundSetCreativeModeSlotPacket((short) containerSlot, stack)); + mc.player.getInventory().setItem(slot, stack); + AutismClientMessaging.sendPrefixed("§aGave §f" + id + " x" + count); + return SUCCESS; + } +} diff --git a/src/main/java/autismclient/commands/impl/HClipCommand.java b/src/main/java/autismclient/commands/impl/HClipCommand.java new file mode 100644 index 0000000..5675965 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/HClipCommand.java @@ -0,0 +1,132 @@ +package autismclient.commands.impl; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.Command; +import autismclient.commands.CommandSuggest; +import autismclient.util.AutismClientMessaging; +import autismclient.util.macro.HClipAction; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; + +public class HClipCommand extends Command { + public HClipCommand() { + super("hclip", "Lets you clip through blocks horizontally (forward) with movement packets."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("\u00a7eUsage: \u00a7f" + prefix + "hclip "); + AutismClientMessaging.sendPrefixed("\u00a77Modes: \u00a7fdefault, forward, back, padding, single, custom (paper alias still works)"); + AutismClientMessaging.sendPrefixed("\u00a77Examples: \u00a7f" + prefix + "hclip forward \u00a77or \u00a7f" + prefix + "hclip custom -25 10 20 true true true"); + return SUCCESS; + }); + + root.then(blocksArgument("blocks", options -> options)); + root.then(LiteralArgumentBuilder.literal("forward") + .executes(ctx -> { + HClipAction.Options options = HClipAction.Options.defaults(0.0); + options.mode = HClipAction.Mode.FORWARD; + run(options); + return SUCCESS; + })); + root.then(LiteralArgumentBuilder.literal("back") + .executes(ctx -> { + HClipAction.Options options = HClipAction.Options.defaults(0.0); + options.mode = HClipAction.Mode.BACK; + run(options); + return SUCCESS; + })); + root.then(LiteralArgumentBuilder.literal("default") + .then(blocksArgument("blocks", options -> options))); + root.then(LiteralArgumentBuilder.literal("paper") + .then(blocksArgument("blocks", options -> options))); + root.then(LiteralArgumentBuilder.literal("padding") + .then(blocksArgument("blocks", options -> options))); + root.then(LiteralArgumentBuilder.literal("single") + .then(blocksArgument("blocks", HClipAction.Options::singlePacket))); + root.then(LiteralArgumentBuilder.literal("normal") + .then(blocksArgument("blocks", HClipAction.Options::singlePacket))); + root.then(LiteralArgumentBuilder.literal("custom") + .then(RequiredArgumentBuilder.argument("blocks", DoubleArgumentType.doubleArg()) + .suggests(CommandSuggest::offsets) + .then(RequiredArgumentBuilder.argument("segment", IntegerArgumentType.integer(1, 50)) + .suggests(CommandSuggest::vclipSegments) + .executes(ctx -> { + HClipAction.Options options = HClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, "blocks")); + options.segmentBlocks = IntegerArgumentType.getInteger(ctx, "segment"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("maxPackets", IntegerArgumentType.integer(1, 100)) + .suggests(CommandSuggest::vclipPacketLimits) + .executes(ctx -> { + HClipAction.Options options = HClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, "blocks")); + options.segmentBlocks = IntegerArgumentType.getInteger(ctx, "segment"); + options.maxPackets = IntegerArgumentType.getInteger(ctx, "maxPackets"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("updateLocal", BoolArgumentType.bool()) + .executes(ctx -> { + HClipAction.Options options = customOptions(ctx); + options.updateLocalPosition = BoolArgumentType.getBool(ctx, "updateLocal"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("vehicle", BoolArgumentType.bool()) + .executes(ctx -> { + HClipAction.Options options = customOptions(ctx); + options.updateLocalPosition = BoolArgumentType.getBool(ctx, "updateLocal"); + options.tryVehicleFirst = BoolArgumentType.getBool(ctx, "vehicle"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("forceGround", BoolArgumentType.bool()) + .executes(ctx -> { + HClipAction.Options options = customOptions(ctx); + options.updateLocalPosition = BoolArgumentType.getBool(ctx, "updateLocal"); + options.tryVehicleFirst = BoolArgumentType.getBool(ctx, "vehicle"); + options.forceGrounded = BoolArgumentType.getBool(ctx, "forceGround"); + run(options); + return SUCCESS; + })))))))); + } + + private interface OptionsCustomizer { + HClipAction.Options apply(HClipAction.Options options); + } + + private static RequiredArgumentBuilder blocksArgument(String name, OptionsCustomizer customizer) { + return RequiredArgumentBuilder.argument(name, DoubleArgumentType.doubleArg()) + .suggests(CommandSuggest::offsets) + .executes(ctx -> { + HClipAction.Options options = HClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, name)); + run(customizer.apply(options)); + return SUCCESS; + }); + } + + private static HClipAction.Options customOptions(com.mojang.brigadier.context.CommandContext ctx) { + HClipAction.Options options = HClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, "blocks")); + options.segmentBlocks = IntegerArgumentType.getInteger(ctx, "segment"); + options.maxPackets = IntegerArgumentType.getInteger(ctx, "maxPackets"); + options.maxRoutePackets = options.maxPackets; + return options; + } + + public static void hclip(double blocks) { + run(HClipAction.Options.defaults(blocks)); + } + + private static void run(HClipAction.Options options) { + options.showMessage = true; + HClipAction.perform(Minecraft.getInstance(), options); + } +} diff --git a/src/main/java/autismclient/commands/impl/HelpCommand.java b/src/main/java/autismclient/commands/impl/HelpCommand.java new file mode 100644 index 0000000..b449043 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/HelpCommand.java @@ -0,0 +1,46 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import java.util.Locale; + +public class HelpCommand extends Command { + public HelpCommand() { super("help", "Show usage info for a command (or list all)."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { listAll(); return SUCCESS; }); + root.then(RequiredArgumentBuilder.argument("command", StringArgumentType.word()) + .suggests((ctx, builder) -> { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (Command command : AutismCommands.all()) { + if (command.name().toLowerCase(Locale.ROOT).startsWith(remaining)) builder.suggest(command.name()); + for (String alias : command.aliases()) { + if (alias != null && alias.toLowerCase(Locale.ROOT).startsWith(remaining)) builder.suggest(alias); + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + String name = StringArgumentType.getString(ctx, "command"); + Command cmd = AutismCommands.find(name); + if (cmd == null) { AutismClientMessaging.sendPrefixed("§cNo such command: §f" + name); return SUCCESS; } + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("§b" + prefix + cmd.name() + " §7" + cmd.description()); + if (cmd.aliases().length > 0) AutismClientMessaging.sendPrefixed("§7aliases: §f" + String.join(", ", cmd.aliases())); + return SUCCESS; + })); + } + + private static void listAll() { + String prefix = AutismCommands.effectivePrefix(); + for (Command c : AutismCommands.all()) { + AutismClientMessaging.sendPrefixed("§b" + prefix + c.name() + "§7 - " + c.description()); + } + } +} diff --git a/src/main/java/autismclient/commands/impl/MacroCommand.java b/src/main/java/autismclient/commands/impl/MacroCommand.java new file mode 100644 index 0000000..056646c --- /dev/null +++ b/src/main/java/autismclient/commands/impl/MacroCommand.java @@ -0,0 +1,102 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.args.MacroArgumentType; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismMacroManager; +import autismclient.util.macro.MacroExecutor; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class MacroCommand extends Command { + private static final ConcurrentHashMap RUNNING_LOOPS = new ConcurrentHashMap<>(); + + public MacroCommand() { super("macro", "Run a macro, optionally N times with a delay between starts."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("§eUsage: §f" + prefix + "macro [times] [delayTicks]"); + AutismClientMessaging.sendPrefixed("§7Also: §f" + prefix + "macro stop [name] §7or §f" + prefix + "macro clear"); + return SUCCESS; + }); + + root.then(LiteralArgumentBuilder.literal("stop") + .executes(ctx -> { + stopAllLoops(); + AutismMacroManager.get().stopMacro(); + AutismClientMessaging.sendPrefixed("§eStopped all macros."); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("name", MacroArgumentType.macroName()) + .executes(ctx -> { + String name = MacroArgumentType.get(ctx, "name"); + AtomicBoolean flag = RUNNING_LOOPS.remove(name.toLowerCase()); + if (flag != null) flag.set(false); + MacroExecutor.stopMacro(name); + AutismClientMessaging.sendPrefixed("§eStopped: §f" + name); + return SUCCESS; + }))); + + root.then(LiteralArgumentBuilder.literal("clear") + .executes(ctx -> { stopAllLoops(); AutismClientMessaging.sendPrefixed("§eCleared macro loops."); return SUCCESS; })); + + root.then(RequiredArgumentBuilder.argument("name", MacroArgumentType.macroName()) + .executes(ctx -> startLoop(MacroArgumentType.get(ctx, "name"), 1, 0)) + .then(RequiredArgumentBuilder.argument("times", IntegerArgumentType.integer(1, 100_000)) + .executes(ctx -> startLoop(MacroArgumentType.get(ctx, "name"), IntegerArgumentType.getInteger(ctx, "times"), 0)) + .then(RequiredArgumentBuilder.argument("delayTicks", IntegerArgumentType.integer(0, 20 * 60 * 60)) + .executes(ctx -> startLoop( + MacroArgumentType.get(ctx, "name"), + IntegerArgumentType.getInteger(ctx, "times"), + IntegerArgumentType.getInteger(ctx, "delayTicks")))))); + } + + private static int startLoop(String name, int times, int delayTicks) { + if (AutismMacroManager.get().get(name) == null) { + AutismClientMessaging.sendPrefixed("§cMacro not found: §f" + name); + return SUCCESS; + } + if (times <= 1 && delayTicks <= 0) { + AutismMacroManager.get().executeMacro(name); + return SUCCESS; + } + AtomicBoolean flag = new AtomicBoolean(true); + RUNNING_LOOPS.put(name.toLowerCase(), flag); + long sleepMs = Math.max(0L, delayTicks * 50L); + final int totalTimes = Math.max(1, times); + Thread t = new Thread(() -> { + try { + for (int i = 0; i < totalTimes && flag.get(); i++) { + AutismMacroManager.get().executeMacro(name); + + long start = System.nanoTime(); + long waitTimeoutNanos = 24L * 60L * 60L * 1_000_000_000L; + while (MacroExecutor.isMacroRunning(name) && flag.get() + && System.nanoTime() - start < waitTimeoutNanos) { + Thread.sleep(50); + } + if (!flag.get()) break; + if (i + 1 < totalTimes && sleepMs > 0) Thread.sleep(sleepMs); + } + } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + finally { RUNNING_LOOPS.remove(name.toLowerCase(), flag); } + }, "MacroLoop-" + name); + t.setDaemon(true); + t.start(); + AutismClientMessaging.sendPrefixed("§aQueued " + totalTimes + "x §f" + name + "§a (delay " + delayTicks + "t)"); + return SUCCESS; + } + + private static void stopAllLoops() { + for (AtomicBoolean flag : RUNNING_LOOPS.values()) flag.set(false); + RUNNING_LOOPS.clear(); + } +} diff --git a/src/main/java/autismclient/commands/impl/ModulesCommand.java b/src/main/java/autismclient/commands/impl/ModulesCommand.java new file mode 100644 index 0000000..618952d --- /dev/null +++ b/src/main/java/autismclient/commands/impl/ModulesCommand.java @@ -0,0 +1,27 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.modules.PackModuleRegistry; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; + +public class ModulesCommand extends Command { + public ModulesCommand() { super("modules", "List installed modules.", "features"); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + java.util.List names = PackModuleRegistry.names(); + AutismClientMessaging.sendPrefixed("§e" + names.size() + " modules:"); + StringBuilder line = new StringBuilder(); + for (String n : names) { + if (line.length() > 0) line.append("§7, "); + line.append("§f").append(n); + if (line.length() > 200) { AutismClientMessaging.sendPrefixed(line.toString()); line.setLength(0); } + } + if (line.length() > 0) AutismClientMessaging.sendPrefixed(line.toString()); + return SUCCESS; + }); + } +} diff --git a/src/main/java/autismclient/commands/impl/NbtCommand.java b/src/main/java/autismclient/commands/impl/NbtCommand.java new file mode 100644 index 0000000..e8ba548 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/NbtCommand.java @@ -0,0 +1,79 @@ +package autismclient.commands.impl; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.Command; +import autismclient.gui.screen.AutismOverlayHostScreen; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismItemCommandSerializer; +import autismclient.util.AutismItemNbtInspectOverlay; +import autismclient.util.AutismNotifications; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; + +public class NbtCommand extends Command { + public NbtCommand() { + super("nbt", "Inspect or copy the held item's components (NBT)."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + inspect(); + return SUCCESS; + }); + root.then(LiteralArgumentBuilder.literal("get").executes(ctx -> { + inspect(); + return SUCCESS; + })); + root.then(LiteralArgumentBuilder.literal("copy").executes(ctx -> { + copy(); + return SUCCESS; + })); + } + + private static ItemStack held() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return ItemStack.EMPTY; + ItemStack main = mc.player.getMainHandItem(); + return !main.isEmpty() ? main : mc.player.getOffhandItem(); + } + + private static void inspect() { + ItemStack stack = held(); + if (stack.isEmpty()) { + AutismClientMessaging.sendPrefixed("\u00a7cHold an item in either hand first."); + return; + } + if (!AutismItemNbtInspectOverlay.openGlobal(stack)) { + AutismClientMessaging.sendPrefixed("\u00a7cCould not open the NBT inspector."); + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc != null) { + autismclient.util.AutismItemNbtInspectOverlay overlay = + autismclient.util.AutismItemNbtInspectOverlay.getSharedOverlay(mc.font); + mc.execute(() -> { + + if (mc.screen == null) { + mc.setScreen(new AutismOverlayHostScreen(overlay)); + } + }); + } + } + + private static void copy() { + ItemStack stack = held(); + if (stack.isEmpty()) { + AutismClientMessaging.sendPrefixed("\u00a7cHold an item in either hand first."); + return; + } + try { + Minecraft.getInstance().keyboardHandler.setClipboard(AutismItemCommandSerializer.giveCommand(stack)); + AutismNotifications.copied("Copied /give command."); + } catch (Throwable t) { + AutismNotifications.error("Copy failed."); + } + } +} diff --git a/src/main/java/autismclient/commands/impl/PluginsCommand.java b/src/main/java/autismclient/commands/impl/PluginsCommand.java new file mode 100644 index 0000000..2396b02 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/PluginsCommand.java @@ -0,0 +1,14 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; + +public class PluginsCommand extends Command { + public PluginsCommand() { super("plugins", "Open the plugin scanner (alias of `server plugins`)."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { ServerCommand.openPlugins(); return SUCCESS; }); + } +} diff --git a/src/main/java/autismclient/commands/impl/PrefixCommand.java b/src/main/java/autismclient/commands/impl/PrefixCommand.java new file mode 100644 index 0000000..60ed52e --- /dev/null +++ b/src/main/java/autismclient/commands/impl/PrefixCommand.java @@ -0,0 +1,48 @@ +package autismclient.commands.impl; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.Command; +import autismclient.commands.CommandSuggest; +import autismclient.modules.AutismModule; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismCompatManager; +import autismclient.util.AutismNotifications; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; + +public class PrefixCommand extends Command { + public PrefixCommand() { + super("prefix", "Change the AUTISM command prefix."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("Current prefix: " + AutismCompatManager.effectiveCommandPrefix()); + return SUCCESS; + }); + root.then(LiteralArgumentBuilder.literal("reset").executes(ctx -> { + String prefix = AutismCompatManager.environmentDefaultCommandPrefix(); + AutismModule.get().setCommandPrefix(prefix); + AutismClientMessaging.sendPrefixed("Prefix reset to " + prefix); + return SUCCESS; + })); + root.then(RequiredArgumentBuilder.argument("new", StringArgumentType.word()) + .suggests(CommandSuggest::prefixes) + .executes(ctx -> { + String requested = StringArgumentType.getString(ctx, "new"); + if (!AutismCompatManager.COMMAND_PREFIX_CHOICES.contains(requested)) { + AutismClientMessaging.sendPrefixed("Prefix must be one of: . % - _ * # @ & ="); + return SUCCESS; + } + String selected = AutismCompatManager.normalizeStoredCommandPrefix(requested); + AutismModule.get().setCommandPrefix(selected); + if (!selected.equals(requested)) { + AutismNotifications.warning("Meteor uses '.'. Prefix kept as '%'."); + } + AutismClientMessaging.sendPrefixed("Prefix set to " + selected); + return SUCCESS; + })); + } +} diff --git a/src/main/java/autismclient/commands/impl/SayCommand.java b/src/main/java/autismclient/commands/impl/SayCommand.java new file mode 100644 index 0000000..024b24d --- /dev/null +++ b/src/main/java/autismclient/commands/impl/SayCommand.java @@ -0,0 +1,31 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.util.AutismClientMessaging; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; + +public class SayCommand extends Command { + public SayCommand() { super("say", "Send a chat message."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("§eUsage: " + AutismCommands.effectivePrefix() + "say "); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("message", StringArgumentType.greedyString()) + .executes(ctx -> { + String msg = StringArgumentType.getString(ctx, "message"); + var conn = Minecraft.getInstance().getConnection(); + if (conn == null) { AutismClientMessaging.sendPrefixed("§cNot connected."); return SUCCESS; } + if (msg.startsWith("/")) conn.sendCommand(msg.substring(1)); + else conn.sendChat(msg); + return SUCCESS; + })); + } +} diff --git a/src/main/java/autismclient/commands/impl/SendCommand.java b/src/main/java/autismclient/commands/impl/SendCommand.java new file mode 100644 index 0000000..8ebefec --- /dev/null +++ b/src/main/java/autismclient/commands/impl/SendCommand.java @@ -0,0 +1,85 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.args.PacketClassArgumentType; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismPacketArgumentBuilder; +import autismclient.util.AutismPacketRegistry; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.Packet; + +public class SendCommand extends Command { + public SendCommand() { super("send", "Send a raw C2S packet by class name with optional field=value arguments."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("§eUsage: §f" + AutismCommands.effectivePrefix() + "send [field=value ...]"); + AutismClientMessaging.sendPrefixed("§7Tip: press Tab after the space to search C2S packet classes."); + AutismClientMessaging.sendPrefixed("§7Example: §f" + AutismCommands.effectivePrefix() + "send ServerboundSetCarriedItemPacket slot=0"); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("packet", PacketClassArgumentType.packetClass()) + .executes(ctx -> send(PacketClassArgumentType.get(ctx, "packet"), "")) + .then(RequiredArgumentBuilder.argument("args", StringArgumentType.greedyString()) + .suggests((ctx, builder) -> { + Class> cls = AutismPacketRegistry.getPacket(PacketClassArgumentType.get(ctx, "packet")); + return AutismPacketArgumentBuilder.suggest(cls, builder); + }) + .executes(ctx -> send(PacketClassArgumentType.get(ctx, "packet"), StringArgumentType.getString(ctx, "args"))))); + } + + private int send(String name, String args) { + Class> cls = AutismPacketRegistry.getPacket(name); + if (cls == null) { + AutismClientMessaging.sendPrefixed("§cUnknown packet: §f" + name); + return SUCCESS; + } + if (!AutismPacketRegistry.getC2SPackets().contains(cls)) { + AutismClientMessaging.sendPrefixed("§cRefusing to send non-C2S packet: §f" + name); + return SUCCESS; + } + + AutismPacketArgumentBuilder.PreparedArgs prepared; + try { + prepared = AutismPacketArgumentBuilder.prepare(args); + } catch (IllegalArgumentException e) { + AutismClientMessaging.sendPrefixed("§cBad arguments: " + e.getMessage()); + return SUCCESS; + } + AutismPacketArgumentBuilder.Result result = AutismPacketArgumentBuilder.build(cls, prepared.args()); + if (result.help()) { + sendLines("§e", result.message()); + return SUCCESS; + } + if (!result.ok()) { + sendLines("§c", "Failed to build §f" + name + "§c: " + result.message()); + return SUCCESS; + } + + if (prepared.dryRun()) { + AutismClientMessaging.sendPrefixed("§eBuilt §f" + name + "§7 (" + result.source() + ") §e- dry run, not sent."); + return SUCCESS; + } + var conn = Minecraft.getInstance().getConnection(); + if (conn == null) { + AutismClientMessaging.sendPrefixed("§cNo network connection."); + return SUCCESS; + } + autismclient.util.AutismSharedState.get().sendPacketBypassDelay(conn, result.packet()); + AutismClientMessaging.sendPrefixed("§aSent §f" + name + "§7 (" + result.source() + ")"); + return SUCCESS; + } + + private static void sendLines(String color, String message) { + if (message == null || message.isBlank()) return; + for (String line : message.split("\\R")) { + if (!line.isBlank()) AutismClientMessaging.sendPrefixed(color + line); + } + } +} diff --git a/src/main/java/autismclient/commands/impl/ServerCommand.java b/src/main/java/autismclient/commands/impl/ServerCommand.java new file mode 100644 index 0000000..a8e1be9 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/ServerCommand.java @@ -0,0 +1,53 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.gui.screen.AutismOverlayHostScreen; +import autismclient.modules.AutismModule; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismOverlayManager; +import autismclient.util.AutismServerInfoOverlay; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.client.Minecraft; + +public class ServerCommand extends Command { + public ServerCommand() { super("server", "Open the server info panel, or the plugin scanner."); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { openInfo(); return SUCCESS; }); + root.then(LiteralArgumentBuilder.literal("info").executes(ctx -> { openInfo(); return SUCCESS; })); + root.then(LiteralArgumentBuilder.literal("plugins").executes(ctx -> { openPlugins(); return SUCCESS; })); + root.then(LiteralArgumentBuilder.literal("tps").executes(ctx -> { tps(); return SUCCESS; })); + } + + static void openInfo() { + openOverlay(false); + } + + static void openPlugins() { + openOverlay(true); + } + + private static void openOverlay(boolean pluginsTab) { + AutismServerInfoOverlay overlay = AutismModule.get().getServerDataOverlay(); + if (overlay == null) { AutismClientMessaging.sendPrefixed("§cServer overlay unavailable."); return; } + AutismOverlayManager.get().register(overlay); + if (pluginsTab) overlay.openPluginsTab(); + else overlay.openInfoTab(); + + Minecraft mc = Minecraft.getInstance(); + if (mc != null) { + mc.execute(() -> { + if (mc.screen == null) { + mc.setScreen(new AutismOverlayHostScreen(overlay)); + } + }); + } + } + + private static void tps() { + double tps = autismclient.util.macro.ServerTickTracker.getEstimatedTps(); + AutismClientMessaging.sendPrefixed(String.format("§eTPS: §f%.2f §7(estimated)", tps)); + } +} diff --git a/src/main/java/autismclient/commands/impl/ToggleCommand.java b/src/main/java/autismclient/commands/impl/ToggleCommand.java new file mode 100644 index 0000000..f4a9e6a --- /dev/null +++ b/src/main/java/autismclient/commands/impl/ToggleCommand.java @@ -0,0 +1,54 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.CommandSuggest; +import autismclient.commands.args.ModuleArgumentType; +import autismclient.modules.PackHideState; +import autismclient.modules.PackModuleRegistry; +import autismclient.util.AutismClientMessaging; +import autismclient.util.macro.ToggleModuleAction; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; + +public class ToggleCommand extends Command { + public ToggleCommand() { super("toggle", "Toggle a module on/off.", "t"); } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + AutismClientMessaging.sendPrefixed("§eUsage: §f" + AutismCommands.effectivePrefix() + "toggle [on|off|toggle]"); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("module", ModuleArgumentType.moduleName()) + .executes(ctx -> doToggle(ModuleArgumentType.get(ctx, "module"), ToggleModuleAction.ToggleMode.TOGGLE)) + .then(RequiredArgumentBuilder.argument("state", StringArgumentType.word()) + .suggests(CommandSuggest::state) + .executes(ctx -> { + String state = StringArgumentType.getString(ctx, "state").toLowerCase(); + if (!state.equals("on") && !state.equals("enable") && !state.equals("true") && !state.equals("1") + && !state.equals("off") && !state.equals("disable") && !state.equals("false") && !state.equals("0") + && !state.equals("toggle")) { + AutismClientMessaging.sendPrefixed("§cUnknown toggle state: §f" + state); + AutismClientMessaging.sendPrefixed("§7Use §fon§7, §foff§7, or §ftoggle§7."); + return SUCCESS; + } + ToggleModuleAction.ToggleMode mode = switch (state) { + case "on", "enable", "true", "1" -> ToggleModuleAction.ToggleMode.ENABLE; + case "off", "disable", "false", "0" -> ToggleModuleAction.ToggleMode.DISABLE; + default -> ToggleModuleAction.ToggleMode.TOGGLE; + }; + return doToggle(ModuleArgumentType.get(ctx, "module"), mode); + }))); + } + + private static int doToggle(String moduleName, ToggleModuleAction.ToggleMode mode) { + boolean ok = PackModuleRegistry.toggle(moduleName, mode); + if (ok && PackHideState.isHideModuleName(moduleName)) return SUCCESS; + if (ok) AutismClientMessaging.sendPrefixed("§a" + mode.name().toLowerCase() + ": §f" + moduleName); + else AutismClientMessaging.sendPrefixed("§cModule not found: §f" + moduleName); + return SUCCESS; + } +} diff --git a/src/main/java/autismclient/commands/impl/VClipCommand.java b/src/main/java/autismclient/commands/impl/VClipCommand.java new file mode 100644 index 0000000..484e84c --- /dev/null +++ b/src/main/java/autismclient/commands/impl/VClipCommand.java @@ -0,0 +1,129 @@ +package autismclient.commands.impl; + +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.Command; +import autismclient.commands.CommandSuggest; +import autismclient.util.AutismClientMessaging; +import autismclient.util.macro.VClipAction; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; + +public class VClipCommand extends Command { + public VClipCommand() { + super("vclip", "Lets you clip through blocks vertically with movement packets."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("\u00a7eUsage: \u00a7f" + prefix + "vclip "); + AutismClientMessaging.sendPrefixed("\u00a77Modes: \u00a7fdefault, top, bottom, paper, single, custom"); + AutismClientMessaging.sendPrefixed("\u00a77Examples: \u00a7f" + prefix + "vclip top \u00a77or \u00a7f" + prefix + "vclip custom -25 10 20 true true"); + return SUCCESS; + }); + + root.then(blocksArgument("blocks", options -> options)); + root.then(LiteralArgumentBuilder.literal("top") + .executes(ctx -> { + VClipAction.Options options = VClipAction.Options.defaults(0.0); + options.mode = VClipAction.Mode.TOP; + run(options); + return SUCCESS; + })); + root.then(LiteralArgumentBuilder.literal("bottom") + .executes(ctx -> { + VClipAction.Options options = VClipAction.Options.defaults(0.0); + options.mode = VClipAction.Mode.BOTTOM; + run(options); + return SUCCESS; + })); + root.then(LiteralArgumentBuilder.literal("default") + .then(blocksArgument("blocks", options -> options))); + root.then(LiteralArgumentBuilder.literal("paper") + .then(blocksArgument("blocks", options -> options))); + root.then(LiteralArgumentBuilder.literal("single") + .then(blocksArgument("blocks", VClipAction.Options::singlePacket))); + root.then(LiteralArgumentBuilder.literal("normal") + .then(blocksArgument("blocks", VClipAction.Options::singlePacket))); + root.then(LiteralArgumentBuilder.literal("custom") + .then(RequiredArgumentBuilder.argument("blocks", DoubleArgumentType.doubleArg()) + .suggests(CommandSuggest::offsets) + .then(RequiredArgumentBuilder.argument("segment", IntegerArgumentType.integer(1, 50)) + .suggests(CommandSuggest::vclipSegments) + .executes(ctx -> { + VClipAction.Options options = VClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, "blocks")); + options.segmentBlocks = IntegerArgumentType.getInteger(ctx, "segment"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("maxPackets", IntegerArgumentType.integer(1, 100)) + .suggests(CommandSuggest::vclipPacketLimits) + .executes(ctx -> { + VClipAction.Options options = VClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, "blocks")); + options.segmentBlocks = IntegerArgumentType.getInteger(ctx, "segment"); + options.maxPackets = IntegerArgumentType.getInteger(ctx, "maxPackets"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("updateLocal", BoolArgumentType.bool()) + .executes(ctx -> { + VClipAction.Options options = customOptions(ctx); + options.updateLocalPosition = BoolArgumentType.getBool(ctx, "updateLocal"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("vehicle", BoolArgumentType.bool()) + .executes(ctx -> { + VClipAction.Options options = customOptions(ctx); + options.updateLocalPosition = BoolArgumentType.getBool(ctx, "updateLocal"); + options.tryVehicleFirst = BoolArgumentType.getBool(ctx, "vehicle"); + run(options); + return SUCCESS; + }) + .then(RequiredArgumentBuilder.argument("forceGround", BoolArgumentType.bool()) + .executes(ctx -> { + VClipAction.Options options = customOptions(ctx); + options.updateLocalPosition = BoolArgumentType.getBool(ctx, "updateLocal"); + options.tryVehicleFirst = BoolArgumentType.getBool(ctx, "vehicle"); + options.forceGrounded = BoolArgumentType.getBool(ctx, "forceGround"); + run(options); + return SUCCESS; + })))))))); + } + + private interface OptionsCustomizer { + VClipAction.Options apply(VClipAction.Options options); + } + + private static RequiredArgumentBuilder blocksArgument(String name, OptionsCustomizer customizer) { + return RequiredArgumentBuilder.argument(name, DoubleArgumentType.doubleArg()) + .suggests(CommandSuggest::offsets) + .executes(ctx -> { + VClipAction.Options options = VClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, name)); + run(customizer.apply(options)); + return SUCCESS; + }); + } + + private static VClipAction.Options customOptions(com.mojang.brigadier.context.CommandContext ctx) { + VClipAction.Options options = VClipAction.Options.defaults(DoubleArgumentType.getDouble(ctx, "blocks")); + options.segmentBlocks = IntegerArgumentType.getInteger(ctx, "segment"); + options.maxPackets = IntegerArgumentType.getInteger(ctx, "maxPackets"); + return options; + } + + public static void vclip(double blocks) { + run(VClipAction.Options.defaults(blocks)); + } + + private static void run(VClipAction.Options options) { + options.showMessage = true; + VClipAction.perform(Minecraft.getInstance(), options); + } +} diff --git a/src/main/java/autismclient/commands/impl/XCarryCommand.java b/src/main/java/autismclient/commands/impl/XCarryCommand.java new file mode 100644 index 0000000..c98db18 --- /dev/null +++ b/src/main/java/autismclient/commands/impl/XCarryCommand.java @@ -0,0 +1,125 @@ +package autismclient.commands.impl; + +import autismclient.commands.Command; +import autismclient.commands.AutismCommandSource; +import autismclient.commands.AutismCommands; +import autismclient.commands.args.XCarrySlotArgumentType; +import autismclient.util.AutismClientMessaging; +import autismclient.util.AutismSharedState; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.item.ItemStack; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class XCarryCommand extends Command { + public XCarryCommand() { + super("xcarry", + "Stash the held item into an XCarry slot: craft1..craft4 / helmet / chestplate / leggings / boots / offhand / cursor."); + } + + @Override + public void build(LiteralArgumentBuilder root) { + root.executes(ctx -> { + String prefix = AutismCommands.effectivePrefix(); + AutismClientMessaging.sendPrefixed("§eUsage: §f" + prefix + "xcarry "); + AutismClientMessaging.sendPrefixed("§7Slots: §fcraft1§7..§fcraft4§7, §fhelmet§7, §fchestplate§7, §fleggings§7, §fboots§7, §foffhand§7, §fcursor§7."); + return SUCCESS; + }); + root.then(RequiredArgumentBuilder.argument("slot", XCarrySlotArgumentType.slot()) + .executes(ctx -> stash(XCarrySlotArgumentType.get(ctx, "slot")))); + } + + private static int stash(int targetSlot) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer p = mc.player; + if (p == null || mc.gameMode == null) { + AutismClientMessaging.sendPrefixed("§cNot in a world."); + return SUCCESS; + } + + ItemStack heldStack = p.getMainHandItem().copy(); + if (heldStack == null || heldStack.isEmpty()) { + AutismClientMessaging.sendPrefixed("§cHold an item first."); + return SUCCESS; + } + + if (p.containerMenu != p.inventoryMenu) { + AutismClientMessaging.sendPrefixed("§cOpen your inventory first (E), then run the command."); + return SUCCESS; + } + + AbstractContainerMenu container = p.containerMenu; + + int hotbar = p.getInventory().getSelectedSlot(); + int sourceSlotId = 36 + hotbar; + + AutismSharedState shared = AutismSharedState.get(); + boolean prevBypass = shared.isXCarryArmorBypass(); + shared.setXCarryArmorBypass(true); + try { + + mc.gameMode.handleContainerInput(container.containerId, sourceSlotId, 0, ContainerInput.PICKUP, p); + if (container.getCarried().isEmpty()) { + AutismClientMessaging.sendPrefixed("§cCould not pick up the held item."); + return SUCCESS; + } + + String itemName = heldStack.getHoverName().getString(); + String slotName = XCarrySlotArgumentType.displayName(targetSlot); + LinkedHashSet forcedSlots = mergedForcedSlots(shared); + boolean forcedCursor = shared.isXCarryForced() && shared.isXCarryForcedCarryCursor(); + + if (targetSlot == XCarrySlotArgumentType.CURSOR) { + + shared.setXCarryForcedTargets(forcedSlots, true); + shared.setXCarryForced(true); + shared.setXCarryActive(true); + AutismClientMessaging.sendPrefixed("§aHolding §f" + itemName + "§a on cursor (XCarry)."); + return SUCCESS; + } + + if (targetSlot < 0 || targetSlot >= container.slots.size()) { + AutismClientMessaging.sendPrefixed("§cInvalid slot (" + targetSlot + ")."); + + mc.gameMode.handleContainerInput(container.containerId, sourceSlotId, 0, ContainerInput.PICKUP, p); + return SUCCESS; + } + + mc.gameMode.handleContainerInput(container.containerId, targetSlot, 0, ContainerInput.PICKUP, p); + + if (!container.getCarried().isEmpty()) { + mc.gameMode.handleContainerInput(container.containerId, sourceSlotId, 0, ContainerInput.PICKUP, p); + } + + forcedSlots.add(targetSlot); + shared.setXCarryForcedTargets(forcedSlots, forcedCursor); + shared.setXCarryForced(true); + shared.setXCarryActive(true); + AutismClientMessaging.sendPrefixed("§aStashed §f" + itemName + "§a into §f" + slotName + "§a."); + return SUCCESS; + } finally { + shared.setXCarryArmorBypass(prevBypass); + } + } + + private static LinkedHashSet mergedForcedSlots(AutismSharedState shared) { + LinkedHashSet slots = new LinkedHashSet<>(); + if (shared != null && shared.isXCarryForced()) { + Set existing = shared.getXCarryForcedSlotMask(); + if (existing != null) { + for (Integer slot : existing) { + if (slot != null && slot >= 0) slots.add(slot); + } + } + } + return slots; + } +} diff --git a/src/main/java/autismclient/compat/AutismModMenuConfigScreen.java b/src/main/java/autismclient/compat/AutismModMenuConfigScreen.java new file mode 100644 index 0000000..114dc2a --- /dev/null +++ b/src/main/java/autismclient/compat/AutismModMenuConfigScreen.java @@ -0,0 +1,161 @@ +package autismclient.compat; + +import autismclient.gui.screen.AutismAddonsScreen; +import autismclient.modules.AutismModule; +import autismclient.util.AutismBindUtil; +import autismclient.util.AutismCompatManager; +import autismclient.util.AutismConfig; +import autismclient.util.AutismLinks; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.CycleButton; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +public class AutismModMenuConfigScreen extends Screen { + private final Screen parent; + + private final List keybinds = new ArrayList<>(); + private final List