Merge pull request #2492 from goatcorp/api14-rollup
Some checks failed
Build Dalamud / Build on Windows (push) Has been cancelled
Build Dalamud / Check API Compatibility (push) Has been cancelled
Build Dalamud / Deploy dalamud-distrib staging (push) Has been cancelled

[api14] Rollup changes from master
This commit is contained in:
goat 2025-12-07 22:22:08 +01:00 committed by GitHub
commit e8485dee25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 254 additions and 98 deletions

View file

@ -8,7 +8,8 @@ import re
import sys import sys
import json import json
import argparse import argparse
from typing import List, Tuple, Optional import os
from typing import List, Tuple, Optional, Dict, Any
def run_git_command(args: List[str]) -> str: def run_git_command(args: List[str]) -> str:
@ -30,14 +31,14 @@ def get_last_two_tags() -> Tuple[str, str]:
"""Get the latest two git tags.""" """Get the latest two git tags."""
tags = run_git_command(["tag", "--sort=-version:refname"]) tags = run_git_command(["tag", "--sort=-version:refname"])
tag_list = [t for t in tags.split("\n") if t] tag_list = [t for t in tags.split("\n") if t]
# Filter out old tags that start with 'v' (old versioning scheme) # Filter out old tags that start with 'v' (old versioning scheme)
tag_list = [t for t in tag_list if not t.startswith('v')] tag_list = [t for t in tag_list if not t.startswith('v')]
if len(tag_list) < 2: if len(tag_list) < 2:
print("Error: Need at least 2 tags in the repository", file=sys.stderr) print("Error: Need at least 2 tags in the repository", file=sys.stderr)
sys.exit(1) sys.exit(1)
return tag_list[0], tag_list[1] return tag_list[0], tag_list[1]
@ -55,58 +56,144 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
return None return None
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]: def get_repo_info() -> Tuple[str, str]:
"""Get commits between two tags. Returns list of (message, author) tuples.""" """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."""
log_output = run_git_command([ log_output = run_git_command([
"log", "log",
f"{tag2}..{tag1}", f"{tag2}..{tag1}",
"--format=%s|%an|%h" "--format=%H"
]) ])
commits = [] commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
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 return commits
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]: def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
"""Filter out commits matching any of the ignore patterns.""" """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."""
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns] compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
filtered = [] filtered = []
for message, author, sha in commits: for pr in prs:
if not any(pattern.search(message) for pattern in compiled_patterns): if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
filtered.append((message, author, sha)) filtered.append(pr)
return filtered return filtered
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]], def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str: cs_commit_new: Optional[str], cs_commit_old: Optional[str],
owner: str, repo: str) -> str:
"""Generate markdown changelog.""" """Generate markdown changelog."""
# Calculate statistics # Calculate statistics
commit_count = len(commits) pr_count = len(prs)
unique_authors = len(set(author for _, author, _ in commits)) unique_authors = len(set(pr["author"] for pr in prs))
changelog = f"# Dalamud Release v{version}\n\n" 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"We just released Dalamud v{version}, which should be available to users within a few minutes. "
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"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](<https://github.com/goatcorp/Dalamud/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n" changelog += f"[Click here](<https://github.com/{owner}/{repo}/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n"
if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old: 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]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n" changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += f"[Click here](<https://github.com/aers/FFXIVClientStructs/compare/{cs_commit_old}...{cs_commit_new}>) to see all CS changes.\n" changelog += f"[Click here](<https://github.com/aers/FFXIVClientStructs/compare/{cs_commit_old}...{cs_commit_new}>) to see all CS changes.\n"
elif cs_commit_new: elif cs_commit_new:
changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n" changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += "## Dalamud Changes\n\n" changelog += "## Dalamud Changes\n\n"
for message, author, sha in commits: for pr in prs:
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n" changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
return changelog return changelog
@ -117,9 +204,9 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None:
except ImportError: except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr) print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1) sys.exit(1)
filename = f"changelog-v{version}.md" filename = f"changelog-v{version}.md"
# Prepare the payload # Prepare the payload
data = { data = {
"content": f"Dalamud v{version} has been released!", "content": f"Dalamud v{version} has been released!",
@ -130,13 +217,13 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None:
} }
] ]
} }
# Prepare the files # Prepare the files
files = { files = {
"payload_json": (None, json.dumps(data)), "payload_json": (None, json.dumps(data)),
"files[0]": (filename, content.encode('utf-8'), 'text/markdown') "files[0]": (filename, content.encode('utf-8'), 'text/markdown')
} }
try: try:
result = requests.post(webhook_url, files=files) result = requests.post(webhook_url, files=files)
result.raise_for_status() result.raise_for_status()
@ -158,54 +245,64 @@ def main():
required=True, required=True,
help="Discord webhook URL" 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( parser.add_argument(
"--ignore", "--ignore",
action="append", action="append",
default=[], default=[],
help="Regex patterns to ignore commits (can be specified multiple times)" help="Regex patterns to ignore PRs (can be specified multiple times)"
) )
parser.add_argument( parser.add_argument(
"--submodule-path", "--submodule-path",
default="lib/FFXIVClientStructs", default="lib/FFXIVClientStructs",
help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)" help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)"
) )
args = parser.parse_args() args = parser.parse_args()
# Get repository info
owner, repo = get_repo_info()
print(f"Repository: {owner}/{repo}")
# Get the last two tags # Get the last two tags
latest_tag, previous_tag = get_last_two_tags() latest_tag, previous_tag = get_last_two_tags()
print(f"Generating changelog between {previous_tag} and {latest_tag}") print(f"Generating changelog between {previous_tag} and {latest_tag}")
# Get submodule commits at both tags # Get submodule commits at both tags
cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag) cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag)
cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag) cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag)
if cs_commit_new: if cs_commit_new:
print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}") print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}")
if cs_commit_old: if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}") print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
# Get commits between tags # Get PRs between tags
commits = get_commits_between_tags(latest_tag, previous_tag) prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
print(f"Found {len(commits)} commits") prs.reverse()
print(f"Found {len(prs)} PRs")
# Filter commits
filtered_commits = filter_commits(commits, args.ignore) # Filter PRs
print(f"After filtering: {len(filtered_commits)} commits") filtered_prs = filter_prs(prs, args.ignore)
print(f"After filtering: {len(filtered_prs)} PRs")
# Generate changelog # Generate changelog
changelog = generate_changelog(latest_tag, previous_tag, filtered_commits, changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
cs_commit_new, cs_commit_old) cs_commit_new, cs_commit_old, owner, repo)
print("\n" + "="*50) print("\n" + "="*50)
print("Generated Changelog:") print("Generated Changelog:")
print("="*50) print("="*50)
print(changelog) print(changelog)
print("="*50 + "\n") print("="*50 + "\n")
# Post to Discord # Post to Discord
post_to_discord(args.webhook_url, changelog, latest_tag) post_to_discord(args.webhook_url, changelog, latest_tag)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -6,41 +6,43 @@ on:
tags: tags:
- '*' - '*'
permissions: read-all
jobs: jobs:
generate-changelog: generate-changelog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Fetch all history and tags fetch-depth: 0 # Fetch all history and tags
submodules: true # Fetch submodules submodules: true # Fetch submodules
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.14' python-version: '3.14'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests
- name: Generate and post changelog - name: Generate and post changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_TERMINAL_PROMPT: 0
run: | run: |
python .github/generate_changelog.py \ python .github/generate_changelog.py \
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \ --webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
--ignore "^Merge" \ --ignore "Update ClientStructs" \
--ignore "^build:" \ --ignore "^build:"
--ignore "^docs:"
env:
GIT_TERMINAL_PROMPT: 0
- name: Upload changelog as artifact - name: Upload changelog as artifact
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: changelog name: changelog
path: changelog-*.md path: changelog-*.md
if-no-files-found: ignore if-no-files-found: ignore

View file

@ -454,3 +454,9 @@ void veh::raise_external_event(const std::wstring& info)
wcsncpy_s(g_external_event_info, info.c_str(), info_size); wcsncpy_s(g_external_event_info, info.c_str(), info_size);
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr); 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);
}

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>13.0.0.12</DalamudVersion> <DalamudVersion>13.0.0.13</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>

View file

@ -82,8 +82,10 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo = var tsInfo =
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>( JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
dalamud.StartInfo.TroubleshootingPackData); dalamud.StartInfo.TroubleshootingPackData);
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
this.HasModifiedGameDataFiles = this.HasModifiedGameDataFiles =
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
if (this.HasModifiedGameDataFiles) if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);

View file

@ -292,7 +292,6 @@ public sealed class EntryPoint
} }
var pluginInfo = string.Empty; var pluginInfo = string.Empty;
var supportText = ", please visit us on Discord for more help";
try try
{ {
var pm = Service<PluginManager>.GetNullable(); var pm = Service<PluginManager>.GetNullable();
@ -300,9 +299,6 @@ public sealed class EntryPoint
if (plugin != null) if (plugin != null)
{ {
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n"; pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
if (plugin.IsThirdParty)
supportText = string.Empty;
} }
} }
catch catch
@ -310,31 +306,18 @@ public sealed class EntryPoint
// ignored // ignored
} }
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);
if (result == MESSAGEBOX_RESULT.IDYES)
{
Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.ForceSave();
}
Log.CloseAndFlush(); Log.CloseAndFlush();
Environment.Exit(-1);
ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
break; break;
default: default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject); Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush(); Log.CloseAndFlush();
Environment.Exit(-1);
break; break;
} }
Environment.Exit(-1);
} }
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args) private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)

View file

@ -161,7 +161,9 @@ internal class SeStringRenderer : IServiceType
ImFont* font = null; ImFont* font = null;
if (drawParams.Font.HasValue) if (drawParams.Font.HasValue)
font = drawParams.Font.Value; font = drawParams.Font.Value;
if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null)
// API14: Remove commented out code
if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null)
font = ImGui.GetFont(); font = ImGui.GetFont();
if (font is null) if (font is null)
throw new ArgumentException("Specified font is empty."); throw new ArgumentException("Specified font is empty.");

View file

@ -14,8 +14,9 @@ public record struct SeStringDrawParams
/// <see cref="ImGui.GetWindowDrawList"/> (the default).</value> /// <see cref="ImGui.GetWindowDrawList"/> (the default).</value>
/// <remarks> /// <remarks>
/// If this value is set, <see cref="ImGui.Dummy"/> will not be called, and ImGui ID will be ignored. /// If this value is set, <see cref="ImGui.Dummy"/> will not be called, and ImGui ID will be ignored.
/// You <b>must</b> specify a valid draw list and a valid font via <see cref="Font"/> if you set this value, /// You <b>must</b> specify a valid draw list, a valid font via <see cref="Font"/> and <see cref="FontSize"/> if you set this value,
/// since the renderer will not be able to retrieve them from ImGui context. /// since the renderer will not be able to retrieve them from ImGui context.
/// Must be set when drawing off the main thread.
/// </remarks> /// </remarks>
public ImDrawListPtr? TargetDrawList { get; set; } public ImDrawListPtr? TargetDrawList { get; set; }
@ -24,16 +25,20 @@ public record struct SeStringDrawParams
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; } public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
/// <summary>Gets or sets the screen offset of the left top corner.</summary> /// <summary>Gets or sets the screen offset of the left top corner.</summary>
/// <value>Screen offset to draw at, or <c>null</c> to use <see cref="ImGui.GetCursorScreenPos()"/>.</value> /// <value>Screen offset to draw at, or <c>null</c> to use <see cref="ImGui.GetCursorScreenPos()"/>, if no <see cref="TargetDrawList"/>
/// is specified. Otherwise, you must specify it (for example, by passing <see cref="ImGui.GetCursorScreenPos()"/> when passing the window
/// draw list.</value>
public Vector2? ScreenOffset { get; set; } public Vector2? ScreenOffset { get; set; }
/// <summary>Gets or sets the font to use.</summary> /// <summary>Gets or sets the font to use.</summary>
/// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value> /// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value>
/// <remarks>Must be set when specifying a target draw-list or drawing off the main thread.</remarks>
public ImFontPtr? Font { get; set; } public ImFontPtr? Font { get; set; }
/// <summary>Gets or sets the font size.</summary> /// <summary>Gets or sets the font size.</summary>
/// <value>Font size in pixels, or <c>0</c> to use the current ImGui font size <see cref="ImGui.GetFontSize"/>. /// <value>Font size in pixels, or <c>0</c> to use the current ImGui font size <see cref="ImGui.GetFontSize"/>.
/// </value> /// </value>
/// <remarks>Must be set when specifying a target draw-list or drawing off the main thread.</remarks>
public float? FontSize { get; set; } public float? FontSize { get; set; }
/// <summary>Gets or sets the line height ratio.</summary> /// <summary>Gets or sets the line height ratio.</summary>

View file

@ -63,9 +63,22 @@ public unsafe ref struct SeStringDrawState
else else
{ {
this.drawList = ssdp.TargetDrawList.Value; this.drawList = ssdp.TargetDrawList.Value;
this.ScreenOffset = Vector2.Zero; this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero;
this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
$"{nameof(ssdp.FontSize)} must be set to render outside the main thread."); // API14: Remove, always throw
if (ThreadSafety.IsMainThread)
{
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
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.WrapWidth = ssdp.WrapWidth ?? float.MaxValue; this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue;
this.Color = ssdp.Color ?? uint.MaxValue; this.Color = ssdp.Color ?? uint.MaxValue;
this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread. this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.

View file

@ -667,6 +667,8 @@ internal class DalamudInterface : IInternalDisposableService
{ {
if (this.isImGuiDrawDevMenu) 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()) if (ImGui.BeginMainMenuBar())
{ {
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
@ -839,6 +841,11 @@ internal class DalamudInterface : IInternalDisposableService
ImGui.PopStyleVar(); ImGui.PopStyleVar();
} }
if (ImGui.MenuItem("Raise external event through boot"))
{
ErrorHandling.CrashWithContext("Tést");
}
ImGui.EndMenu(); ImGui.EndMenu();
} }

View file

@ -177,6 +177,24 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.SeStringWrapped(this.logkind.Value.Data.Span, this.style); ImGuiHelpers.SeStringWrapped(this.logkind.Value.Data.Span, this.style);
} }
if (ImGui.CollapsingHeader("Draw into drawlist"))
{
ImGuiHelpers.ScaledDummy(100);
ImGui.SetCursorScreenPos(ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding);
var clipMin = ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding;
var clipMax = ImGui.GetItemRectMax() - ImGui.GetStyle().FramePadding;
clipMin.Y = MathF.Max(clipMin.Y, ImGui.GetWindowPos().Y);
clipMax.Y = MathF.Min(clipMax.Y, ImGui.GetWindowPos().Y + ImGui.GetWindowHeight());
var dl = ImGui.GetWindowDrawList();
dl.PushClipRect(clipMin, clipMax);
ImGuiHelpers.CompileSeStringWrapped(
"<icon(1)>Test test<icon(1)>",
new SeStringDrawParams
{ Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl });
dl.PopClipRect();
}
if (ImGui.CollapsingHeader("Addon Table"u8)) if (ImGui.CollapsingHeader("Addon Table"u8))
{ {
if (ImGui.BeginTable("Addon Sheet"u8, 3)) if (ImGui.BeginTable("Addon Sheet"u8, 3))

View file

@ -0,0 +1,21 @@
using System.Runtime.InteropServices;
namespace Dalamud.Utility;
/// <summary>
/// Utilities for handling errors inside Dalamud.
/// </summary>
internal static partial class ErrorHandling
{
/// <summary>
/// Crash the game at this point, and show the crash handler with the supplied context.
/// </summary>
/// <param name="context">The context to show in the crash handler.</param>
public static void CrashWithContext(string context)
{
BootVehRaiseExternalEvent(context);
}
[LibraryImport("Dalamud.Boot.dll", EntryPoint = "BootVehRaiseExternalEventW", StringMarshalling = StringMarshalling.Utf16)]
private static partial void BootVehRaiseExternalEvent(string info);
}

View file

@ -1014,8 +1014,8 @@ int main() {
config.pButtons = buttons; config.pButtons = buttons;
config.cButtons = ARRAYSIZE(buttons); config.cButtons = ARRAYSIZE(buttons);
config.nDefaultButton = IdButtonRestart; config.nDefaultButton = IdButtonRestart;
config.pszExpandedControlText = L"Hide stack trace"; config.pszExpandedControlText = L"Hide further information";
config.pszCollapsedControlText = L"Stack trace for plugin developers"; config.pszCollapsedControlText = L"Further information for developers";
config.pszExpandedInformation = window_log_str.c_str(); config.pszExpandedInformation = window_log_str.c_str();
config.pszWindowTitle = L"Dalamud Crash Handler"; config.pszWindowTitle = L"Dalamud Crash Handler";
config.pRadioButtons = radios; config.pRadioButtons = radios;