[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".github/plugin-release-rules.json",
    "content": "{\n  \"plugins\": []\n}\n"
  },
  {
    "path": ".github/workflows/update-pluginmaster.yml",
    "content": "name: 自动更新 pluginmaster\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 * * * *'\n  release:\n    types:\n      - published\n\npermissions:\n  contents: write\n\nconcurrency:\n  group: update-pluginmaster\n  cancel-in-progress: true\n\nenv:\n  TARGET_BRANCH: main\n  TARGET_WORKTREE: ../pluginmaster-main\n\njobs:\n  update-pluginmaster:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: 检出仓库\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: 准备 main 分支工作区\n        shell: bash\n        run: |\n          rm -rf \"${TARGET_WORKTREE}\"\n          rm -f \"${RUNNER_TEMP}/pluginmaster-main-before.json\"\n\n          git fetch origin \"${TARGET_BRANCH}\" || true\n\n          if git show \"origin/${TARGET_BRANCH}:pluginmaster.json\" > \"${RUNNER_TEMP}/pluginmaster-main-before.json\" 2>/dev/null; then\n            echo \"已读取 main 分支当前的 pluginmaster.json\"\n          else\n            cp pluginmaster.json \"${RUNNER_TEMP}/pluginmaster-main-before.json\"\n            echo \"main 分支当前不存在 pluginmaster.json，先使用当前分支文件初始化\"\n          fi\n\n          git worktree add --detach \"${TARGET_WORKTREE}\"\n          git -C \"${TARGET_WORKTREE}\" checkout --orphan \"${TARGET_BRANCH}\"\n          git -C \"${TARGET_WORKTREE}\" rm -rf --ignore-unmatch .\n          git -C \"${TARGET_WORKTREE}\" clean -fdx\n          cp \"${RUNNER_TEMP}/pluginmaster-main-before.json\" \"${TARGET_WORKTREE}/pluginmaster.json\"\n\n      - name: 更新 pluginmaster.json\n        shell: pwsh\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: ./scripts/Update-PluginMaster.ps1 -PluginMasterPath \"${{ github.workspace }}/../pluginmaster-main/pluginmaster.json\"\n\n      - name: 提交 main 分支变更\n        shell: bash\n        run: |\n          if cmp -s \"${TARGET_WORKTREE}/pluginmaster.json\" \"${RUNNER_TEMP}/pluginmaster-main-before.json\"; then\n            echo \"pluginmaster.json 没有变化\"\n            exit 0\n          fi\n\n          git -C \"${TARGET_WORKTREE}\" config user.name \"github-actions[bot]\"\n          git -C \"${TARGET_WORKTREE}\" config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n          git -C \"${TARGET_WORKTREE}\" add pluginmaster.json\n          git -C \"${TARGET_WORKTREE}\" commit -m \"chore: 自动更新 pluginmaster 版本与下载链接\"\n          git -C \"${TARGET_WORKTREE}\" push --force origin \"HEAD:${TARGET_BRANCH}\"\n"
  },
  {
    "path": "README.md",
    "content": "# DalamudPlugins\n由 **AtmoOmen** 个人维护的 Dalamud 插件仓库\n\nDalamud Plugin Repo Maintained by AtmoOmen\n\n## 链接 / Link\n\n**加速链接 / Link For Chinese Mainland:**\n\n```\nhttps://gh.atmoomen.top/DalamudPlugins/main/pluginmaster.json\n```\n\n**原始链接 / Original Link:**\n\n```\nhttps://raw.githubusercontent.com/AtmoOmen/DalamudPlugins/main/pluginmaster.json\n```\n\n**Daily Routines Discord:**\n\n[![image](https://discordapp.com/api/guilds/1258981591124938762/embed.png?style=banner2)](https://discord.gg/dailyroutines)\n\n\n## 仓库插件列表\n\n| 名称      | 原作者      | 游戏版本 | 描述 | 备注 |\n|----------|----------|----------|----------|----------|\n| [Daily Routines](https://github.com/Dalamud-DailyRoutines) | [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 一些自动化小工具合集 | **个人编写** |\n| [Untarnished Heart](https://github.com/AtmoOmen/UntarnishedHeart) | [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 自动化 T 职副本场次成就刷取工具 | **个人编写** |\n| [vnavmesh](https://github.com/AtmoOmen/ffxiv_navmesh-cn) | [veyn](https://github.com/awgil) | 7.4 | 自动寻路 | 适配国服, 部分汉化 |\n| [GatherBuddyReborn](https://github.com/AtmoOmen/GatherBuddyReborn) | [Ottermandias](https://github.com/Ottermandias)， [CombatReborn](https://github.com/FFXIV-CombatReborn) | 7.4 | Gather Buddy 自动采集版 | 适配国服, 部分汉化 |\n| [Currency Tracker](https://github.com/AtmoOmen/CurrencyTracker) | [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 记录你各种的货币收支情况 | **个人编写** |\n| [VFXEditorCN](https://github.com/AtmoOmen/VFXEditor-CN) | [ocealot](https://github.com/0ceal0t) | 7.4 | 游戏内视觉效果、动画与音频编辑插件 | 适配国服, 基本完全汉化 |\n| [Character Data Sync X](https://github.com/AtmoOmen/Dalamud.CharacterSync) | [goat](https://github.com/goaaats), [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 将你的主角色数据安全地同步到其他角色上 | 基于主库完全重写, 完全汉化 |\n| [HaselDebug](https://github.com/AtmoOmen/HaselDebug) | [Haselnussbomber](https://github.com/Haselnussbomber) | 7.4 | 多功能开发用工具箱 | 适配国服 |\n| [A Realm Recorded Lite](https://github.com/AtmoOmen/ARealmRecordedLite) | [UnknownX](https://github.com/UnknownX7), [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | A Realm Recorded 精简版 | 适配国服, 完全汉化, 完全重写\n| [Raphael.Dalamud](https://github.com/Dalamud-DailyRoutines/Raphael.Dalamud) | - | 7.4 | 生产求解器 Raphael 的 Dalamud 包装 | -\n| [DCTravelerX](https://github.com/Dalamud-DailyRoutines/DCTraveler) | [Loskh](https://github.com/Loskh) | 7.4 | 国服游戏内跨大区插件 | 代码清理, 兼容 DR\n"
  },
  {
    "path": "scripts/Update-PluginMaster.ps1",
    "content": "﻿param(\n    [string]$PluginMasterPath = (Join-Path $PSScriptRoot \"..\\pluginmaster.json\"),\n    [string]$ConfigPath = (Join-Path $PSScriptRoot \"..\\.github\\plugin-release-rules.json\"),\n    [string]$GitHubApiBase = \"https://api.github.com\"\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$script:GitHubHeaders = @{\n    Accept       = \"application/vnd.github+json\"\n    \"User-Agent\" = \"DalamudPlugins-PluginMaster-Updater\"\n}\n\nif (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) {\n    $script:GitHubHeaders.Authorization = \"Bearer $($env:GITHUB_TOKEN)\"\n}\n\n$script:ReleaseCache = @{}\n\nfunction Get-ReadablePath {\n    param(\n        [Parameter(Mandatory)]\n        [string]$Path,\n\n        [Parameter(Mandatory)]\n        [string]$Description\n    )\n\n    if ([string]::IsNullOrWhiteSpace($Path)) {\n        throw \"$Description 路径不能为空\"\n    }\n\n    try {\n        return [string](Resolve-Path -LiteralPath $Path)\n    }\n    catch {\n        throw \"$Description 不存在: $Path\"\n    }\n}\n\nfunction Get-ObjectPropertyValue {\n    param(\n        [Parameter(Mandatory)]\n        [AllowNull()]\n        [object]$InputObject,\n\n        [Parameter(Mandatory)]\n        [string]$PropertyName\n    )\n\n    if ($null -eq $InputObject) {\n        return $null\n    }\n\n    if ($InputObject -is [System.Collections.IDictionary]) {\n        if ($InputObject.Contains($PropertyName)) {\n            return $InputObject[$PropertyName]\n        }\n\n        return $null\n    }\n\n    $property = $InputObject.PSObject.Properties[$PropertyName]\n    if ($null -eq $property) {\n        return $null\n    }\n\n    return $property.Value\n}\n\nfunction Read-JsonFile {\n    param(\n        [Parameter(Mandatory)]\n        [string]$Path,\n\n        [Parameter(Mandatory)]\n        [string]$Description\n    )\n\n    $resolvedPath = Get-ReadablePath -Path $Path -Description $Description\n    $rawContent = Get-Content -Raw -Encoding utf8 -LiteralPath $resolvedPath\n    if ([string]::IsNullOrWhiteSpace($rawContent)) {\n        throw \"$Description 内容为空: $resolvedPath\"\n    }\n\n    try {\n        return @{\n            Path       = $resolvedPath\n            RawContent = $rawContent\n            Json       = $rawContent | ConvertFrom-Json\n        }\n    }\n    catch {\n        throw \"$Description 不是合法的 JSON: $resolvedPath。$($_.Exception.Message)\"\n    }\n}\n\nfunction Normalize-Version {\n    param(\n        [Parameter(Mandatory)]\n        [string]$VersionText\n    )\n\n    $segments = $VersionText.Split(\".\", [StringSplitOptions]::RemoveEmptyEntries)\n    if ($segments.Count -lt 3 -or $segments.Count -gt 4) {\n        throw \"不支持的版本号格式: $VersionText\"\n    }\n\n    while ($segments.Count -lt 4) {\n        $segments += \"0\"\n    }\n\n    return [string]::Join(\".\", $segments)\n}\n\nfunction ConvertTo-VersionObject {\n    param(\n        [Parameter(Mandatory)]\n        [string]$VersionText\n    )\n\n    return [Version](Normalize-Version -VersionText $VersionText)\n}\n\nfunction Get-RepoSlugFromUrl {\n    param(\n        [Parameter(Mandatory)]\n        [string]$RepoUrl\n    )\n\n    $match = [regex]::Match($RepoUrl, \"^https://github\\.com/(?<owner>[^/]+)/(?<repo>[^/?#]+?)(?:\\.git)?/?$\")\n    if (-not $match.Success) {\n        throw \"无法从 RepoUrl 解析 GitHub 仓库: $RepoUrl\"\n    }\n\n    return \"$($match.Groups[\"owner\"].Value)/$($match.Groups[\"repo\"].Value)\"\n}\n\nfunction Get-RuleLookup {\n    param(\n        [AllowNull()]\n        [array]$Rules\n    )\n\n    $lookup = @{}\n    if ($null -eq $Rules) {\n        return $lookup\n    }\n\n    for ($index = 0; $index -lt $Rules.Count; $index++) {\n        $rule = $Rules[$index]\n        if ($null -eq $rule) {\n            Write-Warning \"已跳过空规则，索引为 $index\"\n            continue\n        }\n\n        $internalName = [string](Get-ObjectPropertyValue -InputObject $rule -PropertyName \"internalName\")\n        if ([string]::IsNullOrWhiteSpace($internalName)) {\n            Write-Warning \"已跳过缺少 internalName 的规则，索引为 $index\"\n            continue\n        }\n\n        if ($lookup.ContainsKey($internalName)) {\n            Write-Warning \"已跳过重复规则 internalName: $internalName\"\n            continue\n        }\n\n        $lookup[$internalName] = $rule\n    }\n\n    return $lookup\n}\n\nfunction Get-Releases {\n    param(\n        [Parameter(Mandatory)]\n        [string]$RepoSlug\n    )\n\n    if ($script:ReleaseCache.ContainsKey($RepoSlug)) {\n        return $script:ReleaseCache[$RepoSlug]\n    }\n\n    $uri = \"$GitHubApiBase/repos/$RepoSlug/releases?per_page=100\"\n    Write-Host \"正在读取仓库 $RepoSlug 的 release 列表...\"\n    $releases = Invoke-RestMethod -Method Get -Headers $script:GitHubHeaders -Uri $uri\n    $script:ReleaseCache[$RepoSlug] = @($releases)\n    return $script:ReleaseCache[$RepoSlug]\n}\n\nfunction Get-LatestReleaseInfo {\n    param(\n        [Parameter(Mandatory)]\n        [string]$RepoSlug,\n\n        [string]$TagPrefix\n    )\n\n    if ([string]::IsNullOrWhiteSpace($TagPrefix)) {\n        $normalizedPrefix = $null\n    }\n    else {\n        $normalizedPrefix = $TagPrefix.Trim()\n    }\n    $tagPattern = if ($null -eq $normalizedPrefix) {\n        \"^(?<version>\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?)$\"\n    }\n    else {\n        \"^{0}-(?<version>\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?)$\" -f [regex]::Escape($normalizedPrefix)\n    }\n\n    $releases = Get-Releases -RepoSlug $RepoSlug\n    $orderedReleases = $releases |\n        Where-Object { -not $_.draft -and -not $_.prerelease } |\n        Sort-Object {\n            if ($_.published_at) {\n                [DateTimeOffset]$_.published_at\n            }\n            elseif ($_.created_at) {\n                [DateTimeOffset]$_.created_at\n            }\n            else {\n                [DateTimeOffset]::MinValue\n            }\n        } -Descending\n\n    foreach ($release in $orderedReleases) {\n        $match = [regex]::Match([string]$release.tag_name, $tagPattern)\n        if (-not $match.Success) {\n            continue\n        }\n\n        return @{\n            TagName          = [string]$release.tag_name\n            AssemblyVersion  = Normalize-Version -VersionText $match.Groups[\"version\"].Value\n            PublishedAt      = [string]$release.published_at\n        }\n    }\n\n    if ($null -eq $normalizedPrefix) {\n        $modeDescription = \"纯数字 Tag\"\n    }\n    else {\n        $modeDescription = \"前缀为 $normalizedPrefix 的 Tag\"\n    }\n    throw \"仓库 $RepoSlug 中未找到符合条件的最新正式版 release: $modeDescription\"\n}\n\nfunction Get-SelectedPluginIndexes {\n    param(\n        [Parameter(Mandatory)]\n        [AllowEmptyCollection()]\n        [array]$Plugins\n    )\n\n    $selected = @{}\n    for ($index = 0; $index -lt $Plugins.Count; $index++) {\n        $plugin = $Plugins[$index]\n        if ($null -eq $plugin) {\n            Write-Warning \"已跳过空插件对象，索引为 $index\"\n            continue\n        }\n\n        $internalName = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"InternalName\")\n        if ([string]::IsNullOrWhiteSpace($internalName)) {\n            Write-Warning \"已跳过缺少 InternalName 的插件，索引为 $index\"\n            continue\n        }\n\n        $assemblyVersion = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"AssemblyVersion\")\n        if ([string]::IsNullOrWhiteSpace($assemblyVersion)) {\n            Write-Warning \"已跳过缺少 AssemblyVersion 的插件 $internalName\"\n            continue\n        }\n\n        try {\n            $versionObject = ConvertTo-VersionObject -VersionText $assemblyVersion\n        }\n        catch {\n            Write-Warning \"已跳过版本号无效的插件 ${internalName}: $assemblyVersion\"\n            continue\n        }\n\n        if (-not $selected.ContainsKey($internalName) -or $versionObject -gt $selected[$internalName].Version) {\n            $selected[$internalName] = @{\n                Index   = $index\n                Version = $versionObject\n            }\n        }\n    }\n\n    return $selected\n}\n\nfunction Get-DownloadUrl {\n    param(\n        [Parameter(Mandatory)]\n        [string]$RepoSlug,\n\n        [Parameter(Mandatory)]\n        [string]$TagName\n    )\n\n    return \"https://github.com/$RepoSlug/releases/download/$TagName/latest.zip\"\n}\n\nfunction Update-PluginBlock {\n    param(\n        [Parameter(Mandatory)]\n        [string]$Content,\n\n        [Parameter(Mandatory)]\n        [string]$InternalName,\n\n        [Parameter(Mandatory)]\n        [string]$CurrentAssemblyVersion,\n\n        [Parameter(Mandatory)]\n        [string]$NewAssemblyVersion,\n\n        [Parameter(Mandatory)]\n        [string]$DownloadUrl\n    )\n\n    $blockPattern = '(?s)\\{[^{}]*\"InternalName\"\\s*:\\s*\"' + [regex]::Escape($InternalName) + '\"[^{}]*\"AssemblyVersion\"\\s*:\\s*\"' + [regex]::Escape($CurrentAssemblyVersion) + '\"[^{}]*\\}'\n    $blockMatch = [regex]::Match($Content, $blockPattern)\n    if (-not $blockMatch.Success) {\n        throw \"未找到需要回写的插件对象: InternalName=$InternalName, AssemblyVersion=$CurrentAssemblyVersion\"\n    }\n\n    $updatedBlock = $blockMatch.Value\n    $assemblyVersionPattern = New-Object System.Text.RegularExpressions.Regex '(\"AssemblyVersion\"\\s*:\\s*\")[^\"]+(\")'\n    $downloadInstallPattern = New-Object System.Text.RegularExpressions.Regex '(\"DownloadLinkInstall\"\\s*:\\s*\")[^\"]+(\")'\n    $downloadUpdatePattern = New-Object System.Text.RegularExpressions.Regex '(\"DownloadLinkUpdate\"\\s*:\\s*\")[^\"]+(\")'\n\n    $updatedBlock = $assemblyVersionPattern.Replace(\n        $updatedBlock,\n        [System.Text.RegularExpressions.MatchEvaluator]{\n            param($match)\n            return $match.Groups[1].Value + $NewAssemblyVersion + $match.Groups[2].Value\n        },\n        1\n    )\n    $updatedBlock = $downloadInstallPattern.Replace(\n        $updatedBlock,\n        [System.Text.RegularExpressions.MatchEvaluator]{\n            param($match)\n            return $match.Groups[1].Value + $DownloadUrl + $match.Groups[2].Value\n        },\n        1\n    )\n    $updatedBlock = $downloadUpdatePattern.Replace(\n        $updatedBlock,\n        [System.Text.RegularExpressions.MatchEvaluator]{\n            param($match)\n            return $match.Groups[1].Value + $DownloadUrl + $match.Groups[2].Value\n        },\n        1\n    )\n\n    return $Content.Substring(0, $blockMatch.Index) + $updatedBlock + $Content.Substring($blockMatch.Index + $blockMatch.Length)\n}\n\n$pluginMasterFile = Read-JsonFile -Path $PluginMasterPath -Description \"pluginmaster.json\"\n$pluginMasterPath = $pluginMasterFile.Path\n$pluginMasterContent = $pluginMasterFile.RawContent\n$pluginMaster = @($pluginMasterFile.Json)\n\n$configFile = Read-JsonFile -Path $ConfigPath -Description \"规则配置\"\n$config = $configFile.Json\n$configPlugins = @(Get-ObjectPropertyValue -InputObject $config -PropertyName \"plugins\")\n$ruleLookup = Get-RuleLookup -Rules $configPlugins\n$selectedIndexes = Get-SelectedPluginIndexes -Plugins $pluginMaster\n\n$updatedCount = 0\n$skippedCount = 0\n$selectedItems = $selectedIndexes.GetEnumerator() | Sort-Object Key\n$pendingUpdates = @()\n\nforeach ($item in $selectedItems) {\n    try {\n        $pluginIndex = [int]$item.Value.Index\n        $plugin = $pluginMaster[$pluginIndex]\n        $internalName = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"InternalName\")\n\n        $rule = $null\n        if ($ruleLookup.ContainsKey($internalName)) {\n            $rule = $ruleLookup[$internalName]\n        }\n\n        $repoUrl = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"RepoUrl\")\n        $repoSlug = Get-RepoSlugFromUrl -RepoUrl $repoUrl\n        $tagPrefix = $null\n        if ($null -ne $rule) {\n            $sourceRepo = [string](Get-ObjectPropertyValue -InputObject $rule -PropertyName \"sourceRepo\")\n            if (-not [string]::IsNullOrWhiteSpace($sourceRepo)) {\n                $repoSlug = $sourceRepo.Trim()\n            }\n\n            $configuredTagPrefix = [string](Get-ObjectPropertyValue -InputObject $rule -PropertyName \"tagPrefix\")\n            if (-not [string]::IsNullOrWhiteSpace($configuredTagPrefix)) {\n                $tagPrefix = $configuredTagPrefix.Trim()\n            }\n        }\n\n        $latestRelease = Get-LatestReleaseInfo -RepoSlug $repoSlug -TagPrefix $tagPrefix\n        $downloadUrl = Get-DownloadUrl -RepoSlug $repoSlug -TagName $latestRelease.TagName\n\n        $beforeVersion = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"AssemblyVersion\")\n        $beforeInstall = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"DownloadLinkInstall\")\n        $beforeUpdate = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName \"DownloadLinkUpdate\")\n\n        $plugin.AssemblyVersion = $latestRelease.AssemblyVersion\n        $plugin.DownloadLinkInstall = $downloadUrl\n        $plugin.DownloadLinkUpdate = $downloadUrl\n\n        $hasChanged = $false\n        if ($beforeVersion -ne [string]$plugin.AssemblyVersion) {\n            $hasChanged = $true\n        }\n\n        if ($beforeInstall -ne [string]$plugin.DownloadLinkInstall) {\n            $hasChanged = $true\n        }\n\n        if ($beforeUpdate -ne [string]$plugin.DownloadLinkUpdate) {\n            $hasChanged = $true\n        }\n\n        if ($hasChanged) {\n            $updatedCount++\n            $pendingUpdates += [pscustomobject]@{\n                InternalName           = $internalName\n                CurrentAssemblyVersion = $beforeVersion\n                NewAssemblyVersion     = [string]$plugin.AssemblyVersion\n                DownloadUrl            = $downloadUrl\n            }\n            Write-Host \"已更新 $internalName -> 版本 $($plugin.AssemblyVersion)，Tag 为 $($latestRelease.TagName)\"\n        }\n        else {\n            Write-Host \"$internalName 已是最新状态\"\n        }\n    }\n    catch {\n        $skippedCount++\n        Write-Warning \"已跳过插件 $($item.Key): $($_.Exception.Message)\"\n    }\n}\n\nif ($updatedCount -eq 0) {\n    if ($skippedCount -gt 0) {\n        Write-Warning \"本次有 $skippedCount 个插件被跳过\"\n    }\n\n    Write-Host \"pluginmaster.json 无需更新\"\n    exit 0\n}\n\n$updatedContent = $pluginMasterContent\nforeach ($pendingUpdate in $pendingUpdates) {\n    $updatedContent = Update-PluginBlock `\n        -Content $updatedContent `\n        -InternalName $pendingUpdate.InternalName `\n        -CurrentAssemblyVersion $pendingUpdate.CurrentAssemblyVersion `\n        -NewAssemblyVersion $pendingUpdate.NewAssemblyVersion `\n        -DownloadUrl $pendingUpdate.DownloadUrl\n}\n\n$utf8NoBom = New-Object System.Text.UTF8Encoding($false)\n[System.IO.File]::WriteAllText((Resolve-Path $PluginMasterPath), $updatedContent, $utf8NoBom)\nif ($skippedCount -gt 0) {\n    Write-Warning \"本次有 $skippedCount 个插件被跳过\"\n}\n\nWrite-Host \"pluginmaster.json 已写回，共更新 $updatedCount 个插件条目\"\n"
  }
]