diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index b07100115..5e921fd6e 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -8,8 +8,7 @@ import re import sys import json import argparse -import os -from typing import List, Tuple, Optional, Dict, Any +from typing import List, Tuple, Optional def run_git_command(args: List[str]) -> str: @@ -31,14 +30,14 @@ def get_last_two_tags() -> Tuple[str, str]: """Get the latest two git tags.""" tags = run_git_command(["tag", "--sort=-version:refname"]) tag_list = [t for t in tags.split("\n") if t] - + # Filter out old tags that start with 'v' (old versioning scheme) tag_list = [t for t in tag_list if not t.startswith('v')] if len(tag_list) < 2: print("Error: Need at least 2 tags in the repository", file=sys.stderr) sys.exit(1) - + return tag_list[0], tag_list[1] @@ -56,144 +55,58 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]: return None -def get_repo_info() -> Tuple[str, str]: - """Get repository owner and name from git remote.""" - try: - remote_url = run_git_command(["config", "--get", "remote.origin.url"]) - - # Handle both HTTPS and SSH URLs - # SSH: git@github.com:owner/repo.git - # HTTPS: https://github.com/owner/repo.git - match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url) - if match: - owner = match.group(1) - repo = match.group(2) - return owner, repo - else: - print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr) - sys.exit(1) - except: - print("Error: Could not get git remote URL", file=sys.stderr) - sys.exit(1) - - -def get_commits_between_tags(tag1: str, tag2: str) -> List[str]: - """Get commit SHAs between two tags.""" +def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]: + """Get commits between two tags. Returns list of (message, author) tuples.""" log_output = run_git_command([ "log", f"{tag2}..{tag1}", - "--format=%H" + "--format=%s|%an|%h" ]) - - commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()] + + commits = [] + for line in log_output.split("\n"): + if "|" in line: + message, author, sha = line.split("|", 2) + commits.append((message.strip(), author.strip(), sha.strip())) + return commits -def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]: - """Get PR information for a commit using GitHub API.""" - try: - import requests - except ImportError: - print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr) - sys.exit(1) - - headers = { - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28" - } - - if token: - headers["Authorization"] = f"Bearer {token}" - - url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls" - - try: - response = requests.get(url, headers=headers) - response.raise_for_status() - prs = response.json() - - if prs and len(prs) > 0: - # Return the first PR (most relevant one) - pr = prs[0] - return { - "number": pr["number"], - "title": pr["title"], - "author": pr["user"]["login"], - "url": pr["html_url"] - } - except requests.exceptions.HTTPError as e: - if e.response.status_code == 404: - # Commit might not be associated with a PR - return None - elif e.response.status_code == 403: - print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr) - return None - else: - print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr) - return None - except Exception as e: - print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr) - return None - - return None - - -def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]: - """Get PRs between two tags using GitHub API.""" - commits = get_commits_between_tags(tag1, tag2) - print(f"Found {len(commits)} commits, fetching PR information...") - - prs = [] - seen_pr_numbers = set() - - for i, commit_sha in enumerate(commits, 1): - if i % 10 == 0: - print(f"Progress: {i}/{len(commits)} commits processed...") - - pr_info = get_pr_for_commit(commit_sha, owner, repo, token) - if pr_info and pr_info["number"] not in seen_pr_numbers: - seen_pr_numbers.add(pr_info["number"]) - prs.append(pr_info) - - return prs - - -def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]: - """Filter out PRs matching any of the ignore patterns.""" +def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]: + """Filter out commits matching any of the ignore patterns.""" compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns] - + filtered = [] - for pr in prs: - if not any(pattern.search(pr["title"]) for pattern in compiled_patterns): - filtered.append(pr) - + for message, author, sha in commits: + if not any(pattern.search(message) for pattern in compiled_patterns): + filtered.append((message, author, sha)) + return filtered -def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]], - cs_commit_new: Optional[str], cs_commit_old: Optional[str], - owner: str, repo: str) -> str: +def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]], + cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str: """Generate markdown changelog.""" # Calculate statistics - pr_count = len(prs) - unique_authors = len(set(pr["author"] for pr in prs)) - + commit_count = len(commits) + unique_authors = len(set(author for _, author, _ in commits)) + changelog = f"# Dalamud Release v{version}\n\n" changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. " - changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n" - changelog += f"[Click here]() to see all Dalamud changes.\n\n" - + changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n" + changelog += f"[Click here]() to see all Dalamud changes.\n\n" + if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old: changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n" changelog += f"[Click here]() to see all CS changes.\n" elif cs_commit_new: changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n" - + changelog += "## Dalamud Changes\n\n" - - for pr in prs: - changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n" - + + for message, author, sha in commits: + changelog += f"* {message} (by **{author}** as [`{sha}`]())\n" + return changelog @@ -204,9 +117,9 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None: except ImportError: print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr) sys.exit(1) - + filename = f"changelog-v{version}.md" - + # Prepare the payload data = { "content": f"Dalamud v{version} has been released!", @@ -217,13 +130,13 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None: } ] } - + # Prepare the files files = { "payload_json": (None, json.dumps(data)), "files[0]": (filename, content.encode('utf-8'), 'text/markdown') } - + try: result = requests.post(webhook_url, files=files) result.raise_for_status() @@ -245,64 +158,54 @@ def main(): required=True, help="Discord webhook URL" ) - parser.add_argument( - "--github-token", - default=os.environ.get("GITHUB_TOKEN"), - help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit." - ) parser.add_argument( "--ignore", action="append", default=[], - help="Regex patterns to ignore PRs (can be specified multiple times)" + help="Regex patterns to ignore commits (can be specified multiple times)" ) parser.add_argument( "--submodule-path", default="lib/FFXIVClientStructs", help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)" ) - + args = parser.parse_args() - - # Get repository info - owner, repo = get_repo_info() - print(f"Repository: {owner}/{repo}") - + # Get the last two tags latest_tag, previous_tag = get_last_two_tags() print(f"Generating changelog between {previous_tag} and {latest_tag}") - + # Get submodule commits at both tags cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag) cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag) - + if cs_commit_new: print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}") if cs_commit_old: print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}") - - # Get PRs between tags - prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token) - prs.reverse() - print(f"Found {len(prs)} PRs") - - # Filter PRs - filtered_prs = filter_prs(prs, args.ignore) - print(f"After filtering: {len(filtered_prs)} PRs") - + + # Get commits between tags + commits = get_commits_between_tags(latest_tag, previous_tag) + print(f"Found {len(commits)} commits") + + # Filter commits + filtered_commits = filter_commits(commits, args.ignore) + print(f"After filtering: {len(filtered_commits)} commits") + # Generate changelog - changelog = generate_changelog(latest_tag, previous_tag, filtered_prs, - cs_commit_new, cs_commit_old, owner, repo) - + changelog = generate_changelog(latest_tag, previous_tag, filtered_commits, + cs_commit_new, cs_commit_old) + print("\n" + "="*50) print("Generated Changelog:") print("="*50) print(changelog) print("="*50 + "\n") - + # Post to Discord post_to_discord(args.webhook_url, changelog, latest_tag) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index e62d5f37c..5fed3b1eb 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -6,43 +6,41 @@ on: tags: - '*' -permissions: read-all - jobs: generate-changelog: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history and tags submodules: true # Fetch submodules - + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.14' - + - name: Install dependencies run: | python -m pip install --upgrade pip pip install requests - + - name: Generate and post changelog - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_TERMINAL_PROMPT: 0 run: | python .github/generate_changelog.py \ --webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \ - --ignore "Update ClientStructs" \ - --ignore "^build:" - + --ignore "^Merge" \ + --ignore "^build:" \ + --ignore "^docs:" + env: + GIT_TERMINAL_PROMPT: 0 + - name: Upload changelog as artifact if: always() uses: actions/upload-artifact@v4 with: name: changelog path: changelog-*.md - if-no-files-found: ignore + if-no-files-found: ignore \ No newline at end of file diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 194840e52..50ac9b34c 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -453,9 +453,3 @@ void veh::raise_external_event(const std::wstring& info) wcsncpy_s(g_external_event_info, info.c_str(), info_size); RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr); } - -extern "C" __declspec(dllexport) void BootVehRaiseExternalEventW(LPCWSTR info) -{ - const std::wstring info_wstr(info); - veh::raise_external_event(info_wstr); -} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a50f12d79..d1f730d5e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.13 + 13.0.0.12 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 0f8cb0480..15077f3d8 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -292,6 +292,7 @@ public sealed class EntryPoint } var pluginInfo = string.Empty; + var supportText = ", please visit us on Discord for more help"; try { var pm = Service.GetNullable(); @@ -299,6 +300,9 @@ public sealed class EntryPoint if (plugin != null) { pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n"; + + if (plugin.IsThirdParty) + supportText = string.Empty; } } catch @@ -306,18 +310,31 @@ public sealed class EntryPoint // ignored } - Log.CloseAndFlush(); + const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL; + var result = Windows.Win32.PInvoke.MessageBox( + new HWND(Process.GetCurrentProcess().MainWindowHandle), + $"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?", + "Dalamud", + flags); - ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}"); + if (result == MESSAGEBOX_RESULT.IDYES) + { + Log.Information("User chose to disable plugins on next launch..."); + var config = Service.Get(); + config.PluginSafeMode = true; + config.ForceSave(); + } + + Log.CloseAndFlush(); + Environment.Exit(-1); break; default: Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject); Log.CloseAndFlush(); + Environment.Exit(-1); break; } - - Environment.Exit(-1); } private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 397502b30..87df2da2c 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -161,9 +161,7 @@ internal class SeStringRenderer : IServiceType ImFont* font = null; if (drawParams.Font.HasValue) font = drawParams.Font.Value; - - // API14: Remove commented out code - if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null) + if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null) font = ImGui.GetFont(); if (font is null) throw new ArgumentException("Specified font is empty."); diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs index 972013328..1d8126f3b 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs @@ -14,9 +14,8 @@ public record struct SeStringDrawParams /// (the default). /// /// If this value is set, will not be called, and ImGui ID will be ignored. - /// You must specify a valid draw list, a valid font via and if you set this value, + /// You must specify a valid draw list and a valid font via if you set this value, /// since the renderer will not be able to retrieve them from ImGui context. - /// Must be set when drawing off the main thread. /// public ImDrawListPtr? TargetDrawList { get; set; } @@ -30,13 +29,11 @@ public record struct SeStringDrawParams /// Gets or sets the font to use. /// Font to use, or null to use (the default). - /// Must be set when specifying a target draw-list or drawing off the main thread. public ImFontPtr? Font { get; set; } /// Gets or sets the font size. /// Font size in pixels, or 0 to use the current ImGui font size . /// - /// Must be set when specifying a target draw-list or drawing off the main thread. public float? FontSize { get; set; } /// Gets or sets the line height ratio. diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5e63ef160..11c1120b4 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -64,20 +64,8 @@ public unsafe ref struct SeStringDrawState { this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = Vector2.Zero; - - // API14: Remove, always throw - if (ThreadSafety.IsMainThread) - { - this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); - } - else - { - throw new ArgumentException( - $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); - } - - // this.FontSize = ssdp.FontSize ?? throw new ArgumentException( - // $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); + this.FontSize = ssdp.FontSize ?? throw new ArgumentException( + $"{nameof(ssdp.FontSize)} must be set to render outside the main thread."); this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue; this.Color = ssdp.Color ?? uint.MaxValue; this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread. diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index bf55a5486..af78c5b0c 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -667,8 +667,6 @@ internal class DalamudInterface : IInternalDisposableService { if (this.isImGuiDrawDevMenu) { - using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f)); - barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero); if (ImGui.BeginMainMenuBar()) { var pluginManager = Service.Get(); @@ -841,11 +839,6 @@ internal class DalamudInterface : IInternalDisposableService ImGui.PopStyleVar(); } - if (ImGui.MenuItem("Raise external event through boot")) - { - ErrorHandling.CrashWithContext("Tést"); - } - ImGui.EndMenu(); } diff --git a/Dalamud/Utility/ErrorHandling.cs b/Dalamud/Utility/ErrorHandling.cs deleted file mode 100644 index 3c025a12e..000000000 --- a/Dalamud/Utility/ErrorHandling.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Dalamud.Utility; - -/// -/// Utilities for handling errors inside Dalamud. -/// -internal static partial class ErrorHandling -{ - /// - /// Crash the game at this point, and show the crash handler with the supplied context. - /// - /// The context to show in the crash handler. - public static void CrashWithContext(string context) - { - BootVehRaiseExternalEvent(context); - } - - [LibraryImport("Dalamud.Boot.dll", EntryPoint = "BootVehRaiseExternalEventW", StringMarshalling = StringMarshalling.Utf16)] - private static partial void BootVehRaiseExternalEvent(string info); -} diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 3955bd983..1feec4b2f 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -1014,8 +1014,8 @@ int main() { config.pButtons = buttons; config.cButtons = ARRAYSIZE(buttons); config.nDefaultButton = IdButtonRestart; - config.pszExpandedControlText = L"Hide further information"; - config.pszCollapsedControlText = L"Further information for developers"; + config.pszExpandedControlText = L"Hide stack trace"; + config.pszCollapsedControlText = L"Stack trace for plugin developers"; config.pszExpandedInformation = window_log_str.c_str(); config.pszWindowTitle = L"Dalamud Crash Handler"; config.pRadioButtons = radios;