Compare commits

...

20 commits

Author SHA1 Message Date
goat
e8485dee25
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
2025-12-07 22:22:08 +01:00
github-actions[bot]
0072f49fe8 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-12-07 21:13:12 +00:00
goaaats
c45c6aafe1 Don't consider failed index integrity checks as having "modified game data files" 2025-12-07 21:57:54 +01:00
goaaats
2029a0f8a6 Also add fallback for SeStringDrawState.ScreenOffset for now, make sure that it is populated 2025-12-07 21:31:25 +01:00
goat
bcb8094c2d
Merge pull request #2497 from Haselnussbomber/update-fontawesome
[API14] Update Font Awesome to 7.1.0
2025-12-07 17:06:43 +01:00
Haselnussbomber
624191d1e0
Update DalamudAssetPath to FontAwesome710FreeSolid.otf 2025-12-07 16:45:40 +01:00
Haselnussbomber
c254c8600e
Update Font Awesome to 7.1.0 2025-12-07 16:31:03 +01:00
goat
61376fe84e
Merge pull request #2496 from Haselnussbomber/remove-targets
[API14] Remove targets
2025-12-07 16:25:32 +01:00
Haselnussbomber
2f5f52b572
Forgot to remove this too 2025-12-07 16:23:13 +01:00
Haselnussbomber
7199bfb0a9
Remove targets 2025-12-07 16:20:55 +01:00
goat
abcddde591
Merge pull request #2494 from Haselnussbomber/remove-obsoleted-sestring-casts
[API14] Remove obsolete casts from Lumina.Text.SeString
2025-12-07 15:59:33 +01:00
goat
2a99108eb1
Merge pull request #2495 from Haselnussbomber/expose-settings-uibuilder
[API14] Add PluginUISoundEffectsEnabled to UiBuilder
2025-12-07 15:58:58 +01:00
Haselnussbomber
8a5f1fd96d
Add PluginUISoundEffectsEnabled to UiBuilder 2025-12-07 15:55:43 +01:00
goaaats
652ff59672 build: 13.0.0.13
Some checks failed
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Failing after 2s
2025-12-07 15:52:26 +01:00
goaaats
094483e5a0 List PRs in changelog generator 2025-12-07 15:52:13 +01:00
goaaats
c50237cf66 Add compatibility changes for SeString API breakage 2025-12-07 15:46:01 +01:00
Haselnussbomber
d4fe523d73
Clean up some warnings 2025-12-07 15:38:10 +01:00
Haselnussbomber
9e5723359a
Remove obsolete casts from Lumina.Text.SeString 2025-12-07 15:35:38 +01:00
goaaats
b35faf13b5 Show unhandled exceptions through VEH 2025-12-07 13:04:11 +01:00
goaaats
caa869d3ac Clarify exception and docs regarding off-thread drawing with SeStrings, again 2025-12-07 12:54:13 +01:00
22 changed files with 946 additions and 673 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>
@ -227,9 +227,4 @@
<!-- writes the attribute to the customAssemblyInfo file --> <!-- writes the attribute to the customAssemblyInfo file -->
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" /> <WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
</Target> </Target>
<!-- Copy plugin .targets folder into distrib -->
<Target Name="CopyPluginTargets" AfterTargets="Build">
<Copy SourceFiles="$(ProjectDir)\..\targets\Dalamud.Plugin.targets;$(ProjectDir)\..\targets\Dalamud.Plugin.Bootstrap.targets" DestinationFolder="$(OutDir)\targets" />
</Target>
</Project> </Project>

View file

@ -151,7 +151,7 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid. /// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary> /// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)] [DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")] [DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")]
FontAwesomeFreeSolid = 2003, FontAwesomeFreeSolid = 2003,
/// <summary> /// <summary>

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

@ -3,7 +3,6 @@ using System.Globalization;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using LSeString = Lumina.Text.SeString;
namespace Dalamud.Game.Text.Evaluator; namespace Dalamud.Game.Text.Evaluator;
@ -71,9 +70,6 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value)); public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode())); public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
public static implicit operator SeStringParameter(string value) => new(value); public static implicit operator SeStringParameter(string value) => new(value);

View file

@ -113,14 +113,6 @@ public class SeString
/// <returns>Equivalent SeString.</returns> /// <returns>Equivalent SeString.</returns>
public static implicit operator SeString(string str) => new(new TextPayload(str)); public static implicit operator SeString(string str) => new(new TextPayload(str));
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary> /// <summary>
/// Parse a binary game message into an SeString. /// Parse a binary game message into an SeString.
/// </summary> /// </summary>

File diff suppressed because it is too large Load diff

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

@ -219,6 +219,12 @@ public interface IUiBuilder
/// </summary> /// </summary>
bool ShouldUseReducedMotion { get; } bool ShouldUseReducedMotion { get; }
/// <summary>
/// Gets a value indicating whether the user has enabled the "Enable sound effects for plugin windows" setting.<br />
/// This setting is effected by the in-game "System Sounds" option and volume.
/// </summary>
bool PluginUISoundEffectsEnabled { get; }
/// <summary> /// <summary>
/// Loads an ULD file that can load textures containing multiple icons in a single texture. /// Loads an ULD file that can load textures containing multiple icons in a single texture.
/// </summary> /// </summary>
@ -560,6 +566,9 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
/// </summary> /// </summary>
public bool ShouldUseReducedMotion => Service<DalamudConfiguration>.Get().ReduceMotions ?? false; public bool ShouldUseReducedMotion => Service<DalamudConfiguration>.Get().ReduceMotions ?? false;
/// <inheritdoc />
public bool PluginUISoundEffectsEnabled => Service<DalamudConfiguration>.Get().EnablePluginUISoundEffects;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// Gets or sets a value indicating whether statistics about UI draw time should be collected.
/// </summary> /// </summary>

View file

@ -908,7 +908,7 @@ public abstract class Window
private void DrawErrorMessage() private void DrawErrorMessage()
{ {
// TODO: Once window systems are services, offer to reload the plugin // TODO: Once window systems are services, offer to reload the plugin
ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details."));
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);

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

@ -158,16 +158,6 @@ public static partial class Util
return branchInternal = gitBranch; return branchInternal = gitBranch;
} }
/// <summary>
/// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version
/// downloaded from webservices.
/// </summary>
/// <returns>The name of the track, or null.</returns>
internal static string? GetActiveTrack()
{
return Environment.GetEnvironmentVariable("DALAMUD_BRANCH");
}
/// <inheritdoc cref="DescribeAddress(nint)"/> /// <inheritdoc cref="DescribeAddress(nint)"/>
public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p);
@ -703,6 +693,16 @@ public static partial class Util
} }
} }
/// <summary>
/// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version
/// downloaded from webservices.
/// </summary>
/// <returns>The name of the track, or null.</returns>
internal static string? GetActiveTrack()
{
return Environment.GetEnvironmentVariable("DALAMUD_BRANCH");
}
/// <summary> /// <summary>
/// Gets a random, inoffensive, human-friendly string. /// Gets a random, inoffensive, human-friendly string.
/// </summary> /// </summary>

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;

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Linux'))">$(HOME)/.xlcore/dalamud/Hooks/dev/</DalamudLibPath>
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/</DalamudLibPath>
<DalamudLibPath Condition="$(DALAMUD_HOME) != ''">$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<Import Project="$(DalamudLibPath)/targets/Dalamud.Plugin.targets"/>
</Project>

View file

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Platforms>x64</Platforms>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AssemblySearchPaths>$(AssemblySearchPaths);$(DalamudLibPath)</AssemblySearchPaths>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="11.0.0" />
<Reference Include="FFXIVClientStructs" Private="false" />
<Reference Include="Newtonsoft.Json" Private="false" />
<Reference Include="Dalamud" Private="false" />
<Reference Include="ImGui.NET" Private="false" />
<Reference Include="ImGuiScene" Private="false" />
<Reference Include="Lumina" Private="false" />
<Reference Include="Lumina.Excel" Private="false" />
<Reference Include="Serilog" Private="false" />
</ItemGroup>
<Target Name="Message" BeforeTargets="BeforeBuild">
<Message Text="Dalamud.Plugin: root at $(DalamudLibPath)" Importance="high" />
</Target>
<Target Name="DeprecationNotice" BeforeTargets="BeforeBuild">
<Warning Text="Using the targets file to include the Dalamud SDK is no longer recommended. Please upgrade to Dalamud.NET.Sdk - learn more here: https://dalamud.dev/plugin-development/how-tos/v12-sdk-migration" />
</Target>
</Project>