Merge branch 'api14' into AddonLifecycleRefactor

This commit is contained in:
MidoriKami 2025-12-10 14:16:56 -08:00 committed by GitHub
commit 4d9751ea5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 2359 additions and 1509 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

@ -1,9 +1,10 @@
name: Build Dalamud name: Build Dalamud
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
# Globally blocking because of git pushes in deploy step
concurrency: concurrency:
group: build_dalamud_${{ github.ref_name }} group: build_dalamud_${{ github.repository_owner }}
cancel-in-progress: true cancel-in-progress: false
jobs: jobs:
build: build:

View file

@ -1,19 +1,57 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": { "definitions": {
"build": { "Host": {
"type": "object", "type": "string",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"ExecutableTarget": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"Restore",
"SetCILogging",
"Test"
]
},
"Verbosity": {
"type": "string",
"description": "",
"enum": [
"Verbose",
"Normal",
"Minimal",
"Quiet"
]
},
"NukeBuild": {
"properties": { "properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": { "Continue": {
"type": "boolean", "type": "boolean",
"description": "Indicates to continue a previously failed build attempt" "description": "Indicates to continue a previously failed build attempt"
@ -23,29 +61,8 @@
"description": "Shows the help text for this build assembly" "description": "Shows the help text for this build assembly"
}, },
"Host": { "Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'", "description": "Host for execution. Default is 'automatic'",
"enum": [ "$ref": "#/definitions/Host"
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
}, },
"NoLogo": { "NoLogo": {
"type": "boolean", "type": "boolean",
@ -74,63 +91,46 @@
"type": "array", "type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies", "description": "List of targets to be skipped. Empty list skips all dependencies",
"items": { "items": {
"type": "string", "$ref": "#/definitions/ExecutableTarget"
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"Restore",
"SetCILogging",
"Test"
]
} }
}, },
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": { "Target": {
"type": "array", "type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'", "description": "List of targets to be invoked. Default is '{default_target}'",
"items": { "items": {
"type": "string", "$ref": "#/definitions/ExecutableTarget"
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"Restore",
"SetCILogging",
"Test"
]
} }
}, },
"Verbosity": { "Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'", "description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [ "$ref": "#/definitions/Verbosity"
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
} }
} }
} }
} },
} "allOf": [
{
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
}
}
},
{
"$ref": "#/definitions/NukeBuild"
}
]
}

View file

@ -6,6 +6,8 @@
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <Windows.h> #include <Windows.h>
#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
struct exception_info struct exception_info
{ {
LPEXCEPTION_POINTERS pExceptionPointers; LPEXCEPTION_POINTERS pExceptionPointers;

View file

@ -331,6 +331,51 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("VEH was disabled manually"); logging::I("VEH was disabled manually");
} }
// ============================== CLR Reporting =================================== //
// This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
// was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
// Ideally all of this will go away once they get to it.
static std::shared_ptr<hooks::global_import_hook<decltype(ReportEventW)>> s_report_event_hook;
s_report_event_hook = std::make_shared<hooks::global_import_hook<decltype(ReportEventW)>>(
"advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
s_report_event_hook->set_detour([hook = s_report_event_hook.get()](
HANDLE hEventLog,
WORD wType,
WORD wCategory,
DWORD dwEventID,
PSID lpUserSid,
WORD wNumStrings,
DWORD dwDataSize,
LPCWSTR* lpStrings,
LPVOID lpRawData)-> BOOL {
// Check for CLR Error Event IDs
// https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370
if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow
dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed
{
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
}
if (wNumStrings == 0 || lpStrings == nullptr) {
logging::W("ReportEventW called with no strings.");
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
}
// In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log.
const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
const std::wstring error_details(lpStrings[0]);
veh::raise_external_event(error_details);
return original_ret;
});
logging::I("ReportEventW hook installed.");
// ============================== Dalamud ==================================== // // ============================== Dalamud ==================================== //
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint)) if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))

View file

@ -31,6 +31,8 @@ HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr; HANDLE g_crashhandler_pipe_write = nullptr;
wchar_t g_external_event_info[16384] = L"";
std::recursive_mutex g_exception_handler_mutex; std::recursive_mutex g_exception_handler_mutex;
std::chrono::time_point<std::chrono::system_clock> g_time_start; std::chrono::time_point<std::chrono::system_clock> g_time_start;
@ -191,7 +193,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace; std::wstring stackTrace;
if (!g_clr) if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
{
stackTrace = std::wstring(g_external_event_info);
}
else if (!g_clr)
{ {
stackTrace = L"(no CLR stack trace available)"; stackTrace = L"(no CLR stack trace available)";
} }
@ -252,6 +258,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{ {
// special case for CLR exceptions, always trigger crash handler
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
{
return exception_handler(ex);
}
if (ex->ExceptionRecord->ExceptionCode == 0x12345678) if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{ {
// pass // pass
@ -435,3 +447,16 @@ bool veh::remove_handler()
} }
return false; return false;
} }
void veh::raise_external_event(const std::wstring& info)
{
const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1);
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);
}

View file

@ -4,4 +4,5 @@ namespace veh
{ {
bool add_handler(bool doFullDump, const std::string& workingDirectory); bool add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler(); bool remove_handler();
void raise_external_event(const std::wstring& info);
} }

View file

@ -487,6 +487,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f); public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
#pragma warning disable SA1600
#pragma warning disable SA1516
// XLCore/XoM compatibility until they move it out
public string? DalamudBetaKey { get; set; } = null;
public string? DalamudBetaKind { get; set; }
#pragma warning restore SA1516
#pragma warning restore SA1600
/// <summary> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>

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.11</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>
@ -73,8 +73,6 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MinSharp" /> <PackageReference Include="MinSharp" />
<PackageReference Include="SharpDX.Direct3D11" />
<PackageReference Include="SharpDX.Mathematics" />
<PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" /> <PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" /> <PackageReference Include="Serilog.Sinks.Async" />
@ -123,6 +121,8 @@
<Content Include="licenses.txt"> <Content Include="licenses.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<None Remove="Interface\ImGuiBackend\Renderers\gaussian.hlsl" />
<None Remove="Interface\ImGuiBackend\Renderers\fullscreen-quad.hlsl.bytes" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -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

@ -263,7 +263,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb"); var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}"; var searchPath = $".;{symbolPath}";
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle(); var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess();
// Remove any existing Symbol Handler and Init a new one with our search path added // Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess); Windows.Win32.PInvoke.SymCleanup(currentProcess);
@ -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

@ -9,7 +9,6 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events; namespace Dalamud.Game.Addon.Events;
@ -32,25 +31,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService
private readonly AddonLifecycleEventListener finalizeEventListener; private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly AddonEventManagerAddressResolver address; private readonly Hook<AtkUnitManager.Delegates.UpdateCursor> onUpdateCursor;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers; private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
private AddonCursorType? cursorOverride; private AtkCursor.CursorType? cursorOverride;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonEventManager(TargetSigScanner sigScanner) private AddonEventManager()
{ {
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>(); this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null; this.cursorOverride = null;
this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); this.onUpdateCursor = Hook<AtkUnitManager.Delegates.UpdateCursor>.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener); this.addonLifecycle.RegisterListener(this.finalizeEventListener);
@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.onUpdateCursor.Enable(); this.onUpdateCursor.Enable();
} }
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// Force the game cursor to be the specified cursor. /// Force the game cursor to be the specified cursor.
/// </summary> /// </summary>
/// <param name="cursor">Which cursor to use.</param> /// <param name="cursor">Which cursor to use.</param>
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor;
/// <summary> /// <summary>
/// Un-forces the game cursor. /// Un-forces the game cursor.
@ -168,7 +161,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
} }
} }
private nint UpdateCursorDetour(RaptureAtkModule* module) private void UpdateCursorDetour(AtkUnitManager* thisPtr)
{ {
try try
{ {
@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService
if (this.cursorOverride is not null && atkStage is not null) if (this.cursorOverride is not null && atkStage is not null)
{ {
var cursor = (AddonCursorType)atkStage->AtkCursor.Type; ref var atkCursor = ref atkStage->AtkCursor;
if (cursor != this.cursorOverride)
if (atkCursor.Type != this.cursorOverride)
{ {
AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
} }
return nint.Zero; return;
} }
} }
catch (Exception e) catch (Exception e)
@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Error(e, "Exception in UpdateCursorDetour."); Log.Error(e, "Exception in UpdateCursorDetour.");
} }
return this.onUpdateCursor!.Original(module); this.onUpdateCursor!.Original(thisPtr);
} }
} }

View file

@ -1,23 +0,0 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// AddonEventManager memory address resolver.
/// </summary>
internal class AddonEventManagerAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the AtkModule UpdateCursor method.
/// </summary>
public nint UpdateCursor { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="scanner">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner scanner)
{
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
}
}

View file

@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState
public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default; public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default;
/// <inheritdoc/> /// <inheritdoc/>
public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default; public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced; public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;

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

@ -60,8 +60,8 @@ internal record struct NounParams()
/// </summary> /// </summary>
public readonly int ColumnOffset => this.SheetName switch public readonly int ColumnOffset => this.SheetName switch
{ {
// See "E8 ?? ?? ?? ?? 44 8B 6B 08" // See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8"
nameof(LSheets.BeastTribe) => 10, nameof(LSheets.BeastTribe) => 11,
nameof(LSheets.DeepDungeonItem) => 1, nameof(LSheets.DeepDungeonItem) => 1,
nameof(LSheets.DeepDungeonEquipment) => 1, nameof(LSheets.DeepDungeonEquipment) => 1,
nameof(LSheets.DeepDungeonMagicStone) => 1, nameof(LSheets.DeepDungeonMagicStone) => 1,

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>

View file

@ -201,19 +201,19 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook) if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true; useMinHook = true;
using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
if (moduleHandle.IsInvalid) if (moduleHandle.IsNull)
throw new Exception($"Could not get a handle to module {moduleName}"); throw new Exception($"Could not get a handle to module {moduleName}");
var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
if (procAddress == IntPtr.Zero) if (procAddress.IsNull)
throw new Exception($"Could not get the address of {moduleName}::{exportName}"); throw new Exception($"Could not get the address of {moduleName}::{exportName}");
procAddress = HookManager.FollowJmp(procAddress); var address = HookManager.FollowJmp(procAddress.Value);
if (useMinHook) if (useMinHook)
return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly()); return new MinHookHook<T>(address, detour, Assembly.GetCallingAssembly());
else else
return new ReloadedHook<T>(procAddress, detour, Assembly.GetCallingAssembly()); return new ReloadedHook<T>(address, detour, Assembly.GetCallingAssembly());
} }
/// <summary> /// <summary>

File diff suppressed because it is too large Load diff

View file

@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName
var result = new Dictionary<string, string>((int)count); var result = new Dictionary<string, string>((int)count);
for (var i = 0u; i < count; i++) for (var i = 0u; i < count; i++)
{ {
fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError(); fn->GetLocaleName(i, buf, maxStrLen).ThrowOnError();
var key = new string(buf); var key = new string(buf);
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError(); fn->GetString(i, buf, maxStrLen).ThrowOnError();
var value = new string(buf); var value = new string(buf);
result[key.ToLowerInvariant()] = value; result[key.ToLowerInvariant()] = value;
} }

View file

@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId
var familyIndex = 0u; var familyIndex = 0u;
BOOL exists = false; BOOL exists = false;
fixed (void* pName = this.EnglishName) fixed (char* pName = this.EnglishName)
sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError(); sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError();
if (!exists) if (!exists)
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found."); throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");

View file

@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId
var familyIndex = 0u; var familyIndex = 0u;
BOOL exists = false; BOOL exists = false;
fixed (void* name = this.Family.EnglishName) fixed (char* name = this.Family.EnglishName)
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError(); sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError();
if (!exists) if (!exists)
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found."); throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
@ -151,7 +151,7 @@ public sealed class SystemFontId : IFontId
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError(); flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
var path = stackalloc char[(int)pathSize + 1]; var path = stackalloc char[(int)pathSize + 1];
flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError(); flocal.Get()->GetFilePathFromKey(refKey, refKeySize, path, pathSize + 1).ThrowOnError();
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex()); return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
} }

View file

@ -104,19 +104,19 @@ internal static unsafe class ReShadePeeler
fixed (byte* pfn5 = "glBegin"u8) fixed (byte* pfn5 = "glBegin"u8)
fixed (byte* pfn6 = "vkCreateDevice"u8) fixed (byte* pfn6 = "vkCreateDevice"u8)
{ {
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == null)
continue; continue;
} }

View file

@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle) private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
{ {
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW); style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
exStyle = exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP; exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
if (flags.HasFlag(ImGuiViewportFlags.TopMost)) if ((flags & ImGuiViewportFlags.TopMost) != 0)
exStyle |= WS.WS_EX_TOPMOST; exStyle |= WS.WS_EX_TOPMOST;
if ((flags & ImGuiViewportFlags.NoInputs) != 0)
exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
} }
} }

View file

@ -8,6 +8,7 @@ using System.Text;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Utility;
using Serilog; using Serilog;
@ -34,11 +35,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly HCURSOR[] cursors; private readonly HCURSOR[] cursors;
private readonly WndProcDelegate wndProcDelegate; private readonly WndProcDelegate wndProcDelegate;
private readonly bool[] imguiMouseIsDown;
private readonly nint platformNamePtr; private readonly nint platformNamePtr;
private ViewportHandler viewportHandler; private ViewportHandler viewportHandler;
private int mouseButtonsDown;
private bool mouseTracked;
private long lastTime; private long lastTime;
private nint iniPathPtr; private nint iniPathPtr;
@ -64,7 +66,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
ImGuiBackendFlags.HasSetMousePos | ImGuiBackendFlags.HasSetMousePos |
ImGuiBackendFlags.RendererHasViewports | ImGuiBackendFlags.RendererHasViewports |
ImGuiBackendFlags.PlatformHasViewports; ImGuiBackendFlags.PlatformHasViewports |
ImGuiBackendFlags.HasMouseHoveredViewport;
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#"); this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr; io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
@ -74,8 +77,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
this.viewportHandler = new(this); this.viewportHandler = new(this);
this.imguiMouseIsDown = new bool[5];
this.cursors = new HCURSOR[9]; this.cursors = new HCURSOR[9];
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW); this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM); this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
@ -95,8 +96,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam); private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
/// <inheritdoc/> /// <inheritdoc/>
public bool UpdateCursor { get; set; } = true; public bool UpdateCursor { get; set; } = true;
@ -155,6 +154,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
public void NewFrame(int targetWidth, int targetHeight) public void NewFrame(int targetWidth, int targetHeight)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var focusedWindow = GetForegroundWindow();
io.DisplaySize.X = targetWidth; io.DisplaySize.X = targetWidth;
io.DisplaySize.Y = targetHeight; io.DisplaySize.Y = targetHeight;
@ -168,9 +168,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors(); this.viewportHandler.UpdateMonitors();
this.UpdateMousePos(); this.UpdateMouseData(focusedWindow);
this.ProcessKeyEventsWorkarounds(); this.ProcessKeyEventsWorkarounds(focusedWindow);
// TODO: need to figure out some way to unify all this // TODO: need to figure out some way to unify all this
// The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues // The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
@ -224,6 +224,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
switch (msg) switch (msg)
{ {
case WM.WM_MOUSEMOVE:
{
if (!this.mouseTracked)
{
var tme = new TRACKMOUSEEVENT
{
cbSize = (uint)sizeof(TRACKMOUSEEVENT),
dwFlags = TME.TME_LEAVE,
hwndTrack = hWndCurrent,
};
this.mouseTracked = TrackMouseEvent(&tme);
}
var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(hWndCurrent, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
break;
}
case WM.WM_MOUSELEAVE:
{
this.mouseTracked = false;
var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
ClientToScreen(hWndCurrent, &mouseScreenPos);
if (this.ViewportFromPoint(mouseScreenPos).IsNull)
{
var fltMax = ImGuiNative.GETFLTMAX();
io.AddMousePosEvent(-fltMax, -fltMax);
}
break;
}
case WM.WM_LBUTTONDOWN: case WM.WM_LBUTTONDOWN:
case WM.WM_LBUTTONDBLCLK: case WM.WM_LBUTTONDBLCLK:
case WM.WM_RBUTTONDOWN: case WM.WM_RBUTTONDOWN:
@ -236,11 +270,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
var button = GetButton(msg, wParam); var button = GetButton(msg, wParam);
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero) if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
SetCapture(hWndCurrent); SetCapture(hWndCurrent);
this.mouseButtonsDown |= 1 << button;
io.MouseDown[button] = true; io.AddMouseButtonEvent(button, true);
this.imguiMouseIsDown[button] = true;
return default(LRESULT); return default(LRESULT);
} }
@ -256,13 +289,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONUP: case WM.WM_XBUTTONUP:
{ {
var button = GetButton(msg, wParam); var button = GetButton(msg, wParam);
if (io.WantCaptureMouse && this.imguiMouseIsDown[button]) if (io.WantCaptureMouse)
{ {
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) this.mouseButtonsDown &= ~(1 << button);
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
ReleaseCapture(); ReleaseCapture();
io.AddMouseButtonEvent(button, false);
io.MouseDown[button] = false;
this.imguiMouseIsDown[button] = false;
return default(LRESULT); return default(LRESULT);
} }
@ -272,7 +304,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEWHEEL: case WM.WM_MOUSEWHEEL:
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
return default(LRESULT); return default(LRESULT);
} }
@ -280,7 +312,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEHWHEEL: case WM.WM_MOUSEHWHEEL:
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
return default(LRESULT); return default(LRESULT);
} }
@ -374,68 +406,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors(); this.viewportHandler.UpdateMonitors();
break; break;
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd: case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) io.AddFocusEvent(true);
ReleaseCapture(); break;
ImGui.GetIO().WantCaptureMouse = false; case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
ImGui.ClearWindowFocus(); io.AddFocusEvent(false);
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
// ReleaseCapture();
//
// ImGui.GetIO().WantCaptureMouse = false;
// ImGui.ClearWindowFocus();
break; break;
} }
return null; return null;
} }
private void UpdateMousePos() private void UpdateMouseData(HWND focusedWindow)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var pt = default(POINT);
// Depending on if Viewports are enabled, we have to change how we process var mouseScreenPos = default(POINT);
// the cursor position. If viewports are enabled, we pass the absolute cursor var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
// position to ImGui. Otherwise, we use the old method of passing client-local
// mouse position to ImGui. var isAppFocused =
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) focusedWindow != default
&& (focusedWindow == this.hWnd
|| IsChild(focusedWindow, this.hWnd)
|| !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull);
if (isAppFocused)
{ {
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
// When multi-viewports are enabled, all Dear ImGui positions are same as OS positions.
if (io.WantSetMousePos) if (io.WantSetMousePos)
{ {
SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y);
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(this.hWnd, &pos);
SetCursorPos(pos.x, pos.y);
} }
}
if (GetCursorPos(&pt)) // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
{ if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
io.MousePos.X = pt.x; {
io.MousePos.Y = pt.y; // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
} // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
else // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
{ // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
io.MousePos.X = float.MinValue; var mousePos = mouseScreenPos;
io.MousePos.Y = float.MinValue; if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
} ClientToScreen(focusedWindow, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
}
// (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
// If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic.
// - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that)
// Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window
// for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported
// by the backend, and use its flawed heuristic to guess the viewport behind.
// - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target).
if (hasMouseScreenPos)
{
var viewport = this.ViewportFromPoint(mouseScreenPos);
io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
} }
else else
{ {
if (io.WantSetMousePos) io.AddMouseViewportEvent(0);
{
pt.x = (int)io.MousePos.X;
pt.y = (int)io.MousePos.Y;
ClientToScreen(this.hWnd, &pt);
SetCursorPos(pt.x, pt.y);
}
if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
{
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y;
}
else
{
io.MousePos.X = float.MinValue;
io.MousePos.Y = float.MinValue;
}
} }
} }
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
{
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
}
private bool UpdateMouseCursor() private bool UpdateMouseCursor()
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
@ -451,7 +501,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return true; return true;
} }
private void ProcessKeyEventsWorkarounds() private void ProcessKeyEventsWorkarounds(HWND focusedWindow)
{ {
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one. // Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT)) if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
@ -480,7 +530,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
// See: https://github.com/goatcorp/ImGuiScene/pull/13 // See: https://github.com/goatcorp/ImGuiScene/pull/13
// > GetForegroundWindow from winuser.h is a surprisingly expensive function. // > GetForegroundWindow from winuser.h is a surprisingly expensive function.
var isForeground = GetForegroundWindow() == this.hWnd; var isForeground = focusedWindow == this.hWnd;
for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++) for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++)
{ {
// Skip raising modifier keys if the game is focused. // Skip raising modifier keys if the game is focused.
@ -622,7 +672,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND), hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND),
lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal
.GetFunctionPointerForDelegate(this.input.wndProcDelegate), .GetFunctionPointerForDelegate(this.input.wndProcDelegate),
lpszClassName = (ushort*)windowClassNamePtr, lpszClassName = windowClassNamePtr,
}; };
if (RegisterClassExW(&wcex) == 0) if (RegisterClassExW(&wcex) == 0)
@ -646,19 +696,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return; return;
var pio = ImGui.GetPlatformIO(); var pio = ImGui.GetPlatformIO();
ImGui.GetPlatformIO().Handle->Monitors.Free();
if (ImGui.GetPlatformIO().Handle->Monitors.Data != null)
{
// We allocated the platform monitor data in OnUpdateMonitors ourselves,
// so we have to free it ourselves to ImGui doesn't try to, or else it will crash
Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data));
ImGui.GetPlatformIO().Handle->Monitors = default;
}
fixed (char* windowClassNamePtr = WindowClassName) fixed (char* windowClassNamePtr = WindowClassName)
{ {
UnregisterClassW( UnregisterClassW(
(ushort*)windowClassNamePtr, windowClassNamePtr,
(HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module)); (HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module));
} }
@ -693,59 +736,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// Here we use a manual ImVector overload, free the existing monitor data, // Here we use a manual ImVector overload, free the existing monitor data,
// and allocate our own, as we are responsible for telling ImGui about monitors // and allocate our own, as we are responsible for telling ImGui about monitors
var pio = ImGui.GetPlatformIO(); var pio = ImGui.GetPlatformIO();
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS); pio.Handle->Monitors.Resize(0);
var data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
if (pio.Handle->Monitors.Data != null)
Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data));
pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer());
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
// Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data);
// int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS);
// nint data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
// platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data);
var monitorIndex = -1;
var enumfn = new MonitorEnumProcDelegate(
(hMonitor, _, _, _) =>
{
monitorIndex++;
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex);
imMonitor.MainPos = monitorLt;
imMonitor.MainSize = monitorRb - monitorLt;
imMonitor.WorkPos = workLt;
imMonitor.WorkSize = workRb - workLt;
imMonitor.DpiScale = 1f;
return true;
});
EnumDisplayMonitors(
default,
null,
(delegate* unmanaged<HMONITOR, HDC, RECT*, LPARAM, BOOL>)Marshal.GetFunctionPointerForDelegate(enumfn),
default);
Log.Information("Monitors set up!"); Log.Information("Monitors set up!");
for (var i = 0; i < numMonitors; i++) foreach (ref var monitor in pio.Handle->Monitors)
{ {
var monitor = pio.Handle->Monitors[i];
Log.Information( Log.Information(
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}", "Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
i,
monitor.MainPos, monitor.MainPos,
monitor.MainSize, monitor.MainSize,
monitor.WorkPos, monitor.WorkPos,
monitor.WorkSize); monitor.WorkSize);
} }
return;
[UnmanagedCallersOnly]
static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
{
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
var imMonitor = new ImGuiPlatformMonitor
{
MainPos = monitorLt,
MainSize = monitorRb - monitorLt,
WorkPos = workLt,
WorkSize = workRb - workLt,
DpiScale = 1f,
};
if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0)
ImGui.GetPlatformIO().Monitors.PushFront(imMonitor);
else
ImGui.GetPlatformIO().Monitors.PushBack(imMonitor);
return true;
}
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@ -781,8 +815,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
data->Hwnd = CreateWindowExW( data->Hwnd = CreateWindowExW(
(uint)data->DwExStyle, (uint)data->DwExStyle,
(ushort*)windowClassNamePtr, windowClassNamePtr,
(ushort*)windowClassNamePtr, windowClassNamePtr,
(uint)data->DwStyle, (uint)data->DwStyle,
rect.left, rect.left,
rect.top, rect.top,
@ -794,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
null); null);
} }
if (data->Hwnd == 0)
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
data->HwndOwned = true; data->HwndOwned = true;
viewport.PlatformRequestResize = false; viewport.PlatformRequestResize = false;
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;
@ -993,7 +1030,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title)) fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title))
SetWindowTextW(data->Hwnd, (ushort*)pwszTitle); SetWindowTextW(data->Hwnd, pwszTitle);
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]

View file

@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Color stacks to use while evaluating a SeString.</summary> /// <summary>Color stacks to use while evaluating a SeString.</summary>
internal sealed class SeStringColorStackSet internal sealed class SeStringColorStackSet
{ {
/// <summary>Parsed <see cref="UIColor"/>, containing colors to use with <see cref="MacroCode.ColorType"/> and
/// <see cref="MacroCode.EdgeColorType"/>.</summary>
private readonly uint[,] colorTypes;
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary> /// <summary>Foreground color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> colorStack = []; private readonly List<uint> colorStack = [];
@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet
foreach (var row in uiColor) foreach (var row in uiColor)
maxId = (int)Math.Max(row.RowId, maxId); maxId = (int)Math.Max(row.RowId, maxId);
this.colorTypes = new uint[maxId + 1, 4]; this.ColorTypes = new uint[maxId + 1, 4];
foreach (var row in uiColor) foreach (var row in uiColor)
{ {
// Contains ABGR. // Contains ABGR.
this.colorTypes[row.RowId, 0] = row.Dark; this.ColorTypes[row.RowId, 0] = row.Dark;
this.colorTypes[row.RowId, 1] = row.Light; this.ColorTypes[row.RowId, 1] = row.Light;
this.colorTypes[row.RowId, 2] = row.ClassicFF; this.ColorTypes[row.RowId, 2] = row.ClassicFF;
this.colorTypes[row.RowId, 3] = row.ClearBlue; this.ColorTypes[row.RowId, 3] = row.ClearBlue;
} }
if (BitConverter.IsLittleEndian) if (BitConverter.IsLittleEndian)
{ {
// ImGui wants RGBA in LE. // ImGui wants RGBA in LE.
fixed (uint* p = this.colorTypes) fixed (uint* p = this.ColorTypes)
{ {
foreach (ref var r in new Span<uint>(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1))) foreach (ref var r in new Span<uint>(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1)))
r = BinaryPrimitives.ReverseEndianness(r); r = BinaryPrimitives.ReverseEndianness(r);
} }
} }
} }
/// <summary>Initializes a new instance of the <see cref="SeStringColorStackSet"/> class.</summary>
/// <param name="colorTypes">Color types.</param>
public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes;
/// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary> /// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary>
public bool HasAdditionalEdgeColor { get; private set; } public bool HasAdditionalEdgeColor { get; private set; }
/// <summary>Gets the parsed <see cref="UIColor"/> containing colors to use with <see cref="MacroCode.ColorType"/>
/// and <see cref="MacroCode.EdgeColorType"/>.</summary>
public uint[,] ColorTypes { get; }
/// <summary>Resets the colors in the stack.</summary> /// <summary>Resets the colors in the stack.</summary>
/// <param name="drawState">Draw state.</param> /// <param name="drawState">Draw state.</param>
internal void Initialize(scoped ref SeStringDrawState drawState) internal void Initialize(scoped ref SeStringDrawState drawState)
@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet
} }
// Opacity component is ignored. // Opacity component is ignored.
var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) && var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) &&
colorTypeIndex < this.colorTypes.GetLength(0) colorTypeIndex < this.ColorTypes.GetLength(0)
? this.colorTypes[colorTypeIndex, themeIndex] ? this.ColorTypes[colorTypeIndex, themeIndex]
: 0u; : 0u;
rgbaStack.Add(color | 0xFF000000u); rgbaStack.Add(color | 0xFF000000u);

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Draws SeString.</summary> /// <summary>Draws SeString.</summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class SeStringRenderer : IInternalDisposableService internal class SeStringRenderer : IServiceType
{ {
private const int ImGuiContextCurrentWindowOffset = 0x3FF0; private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
private const int ImGuiWindowDcOffset = 0x118; private const int ImGuiWindowDcOffset = 0x118;
@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Parsed text fragments from a SeString.</summary> /// <summary>Parsed text fragments from a SeString.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly List<TextFragment> fragments = []; private readonly List<TextFragment> fragmentsMainThread = [];
/// <summary>Color stacks to use while evaluating a SeString for rendering.</summary> /// <summary>Color stacks to use while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly SeStringColorStackSet colorStackSet; private readonly SeStringColorStackSet colorStackSetMainThread;
/// <summary>Splits a draw list so that different layers of a single glyph can be drawn out of order.</summary>
private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner) private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{ {
this.colorStackSet = new(dm.Excel.GetSheet<UIColor>()); this.colorStackSetMainThread = new(dm.Excel.GetSheet<UIColor>());
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!; this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
} }
/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
~SeStringRenderer() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
/// <summary>Compiles and caches a SeString from a text macro representation.</summary> /// <summary>Compiles and caches a SeString from a text macro representation.</summary>
/// <param name="text">SeString text macro representation. /// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to newline payloads.</param> /// Newline characters will be normalized to newline payloads.</param>
@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
text.ReplaceLineEndings("<br>"), text.ReplaceLineEndings("<br>"),
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError })); new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
/// <param name="sss">SeString to render.</param>
/// <param name="drawParams">Parameters for drawing.</param>
/// <returns>A new self-contained draw data.</returns>
public unsafe BufferBackedImDrawData CreateDrawData(
ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default)
{
if (drawParams.TargetDrawList is not null)
{
throw new ArgumentException(
$"{nameof(SeStringDrawParams.TargetDrawList)} may not be specified.",
nameof(drawParams));
}
var dd = BufferBackedImDrawData.Create();
try
{
var size = this.Draw(sss, drawParams with { TargetDrawList = dd.ListPtr }).Size;
var offset = drawParams.ScreenOffset ?? Vector2.Zero;
foreach (var vtx in new Span<ImDrawVert>(dd.ListPtr.VtxBuffer.Data, dd.ListPtr.VtxBuffer.Size))
offset = Vector2.Min(offset, vtx.Pos);
dd.Data.DisplayPos = offset;
dd.Data.DisplaySize = size - offset;
dd.Data.Valid = 1;
dd.UpdateDrawDataStatistics();
return dd;
}
catch
{
dd.Dispose();
throw;
}
}
/// <summary>Compiles and caches a SeString from a text macro representation, and then draws it.</summary> /// <summary>Compiles and caches a SeString from a text macro representation, and then draws it.</summary>
/// <param name="text">SeString text macro representation. /// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to newline payloads.</param> /// Newline characters will be normalized to newline payloads.</param>
@ -113,28 +143,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param> /// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
/// <param name="buttonFlags">Button flags to use on link interaction.</param> /// <param name="buttonFlags">Button flags to use on link interaction.</param>
/// <returns>Interaction result of the rendered text.</returns> /// <returns>Interaction result of the rendered text.</returns>
public SeStringDrawResult Draw( public unsafe SeStringDrawResult Draw(
ReadOnlySeStringSpan sss, ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default, scoped in SeStringDrawParams drawParams = default,
ImGuiId imGuiId = default, ImGuiId imGuiId = default,
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault)
{ {
// Drawing is only valid if done from the main thread anyway, especially with interactivity. // Interactivity is supported only from the main thread.
ThreadSafety.AssertMainThread(); if (!imGuiId.IsEmpty())
ThreadSafety.AssertMainThread();
if (drawParams.TargetDrawList is not null && imGuiId) if (drawParams.TargetDrawList is not null && imGuiId)
throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId)); throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId));
// This also does argument validation for drawParams. Do it here. using var cleanup = new DisposeSafety.ScopedFinalizer();
var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
// Reset and initialize the state. ImFont* font = null;
this.fragments.Clear(); if (drawParams.Font.HasValue)
this.colorStackSet.Initialize(ref state); font = drawParams.Font.Value;
// API14: Remove commented out code
if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null)
font = ImGui.GetFont();
if (font is null)
throw new ArgumentException("Specified font is empty.");
// This also does argument validation for drawParams. Do it here.
// `using var` makes a struct read-only, but we do want to modify it.
var stateStorage = new SeStringDrawState(
sss,
drawParams,
ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
ThreadSafety.IsMainThread ? this.fragmentsMainThread : [],
font);
ref var state = ref Unsafe.AsRef(in stateStorage);
// Analyze the provided SeString and break it up to text fragments. // Analyze the provided SeString and break it up to text fragments.
this.CreateTextFragments(ref state); this.CreateTextFragments(ref state);
var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments); var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments);
// Calculate size. // Calculate size.
var size = Vector2.Zero; var size = Vector2.Zero;
@ -147,24 +193,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
state.SplitDrawList(); state.SplitDrawList();
// Handle cases where ImGui.AlignTextToFramePadding has been called.
var context = ImGui.GetCurrentContext();
var currLineTextBaseOffset = 0f;
if (!context.IsNull)
{
var currentWindow = context.CurrentWindow;
if (!currentWindow.IsNull)
{
currLineTextBaseOffset = currentWindow.DC.CurrLineTextBaseOffset;
}
}
var itemSize = size; var itemSize = size;
if (currLineTextBaseOffset != 0f) if (drawParams.TargetDrawList is null)
{ {
itemSize.Y += 2 * currLineTextBaseOffset; // Handle cases where ImGui.AlignTextToFramePadding has been called.
foreach (ref var f in fragmentSpan) var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset;
f.Offset += new Vector2(0, currLineTextBaseOffset); if (currLineTextBaseOffset != 0f)
{
itemSize.Y += 2 * currLineTextBaseOffset;
foreach (ref var f in fragmentSpan)
f.Offset += new Vector2(0, currLineTextBaseOffset);
}
} }
// Draw all text fragments. // Draw all text fragments.
@ -280,15 +319,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return displayRune.Value != 0; return displayRune.Value != 0;
} }
private void ReleaseUnmanagedResources()
{
if (this.splitter is not null)
{
this.splitter->Destroy();
this.splitter = null;
}
}
/// <summary>Creates text fragment, taking line and word breaking into account.</summary> /// <summary>Creates text fragment, taking line and word breaking into account.</summary>
/// <param name="state">Draw state.</param> /// <param name="state">Draw state.</param>
private void CreateTextFragments(ref SeStringDrawState state) private void CreateTextFragments(ref SeStringDrawState state)
@ -391,7 +421,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth; var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;
// Test if the fragment does not fit into the current line and the current line is not empty. // Test if the fragment does not fit into the current line and the current line is not empty.
if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows) if (xy.X != 0 && state.Fragments.Count > 0 && !state.Fragments[^1].BreakAfter && overflows)
{ {
// Introduce break if this is the first time testing the current break unit or the current fragment // Introduce break if this is the first time testing the current break unit or the current fragment
// is an entity. // is an entity.
@ -401,7 +431,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
xy.X = 0; xy.X = 0;
xy.Y += state.LineHeight; xy.Y += state.LineHeight;
w = 0; w = 0;
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true; CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true;
fragment.Offset = xy; fragment.Offset = xy;
// Now that the fragment is given its own line, test if it overflows again. // Now that the fragment is given its own line, test if it overflows again.
@ -419,16 +449,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth); fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
} }
} }
else if (this.fragments.Count > 0 && xy.X != 0) else if (state.Fragments.Count > 0 && xy.X != 0)
{ {
// New fragment fits into the current line, and it has a previous fragment in the same line. // New fragment fits into the current line, and it has a previous fragment in the same line.
// If the previous fragment ends with a soft hyphen, adjust its width so that the width of its // If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
// trailing soft hyphens are not considered. // trailing soft hyphens are not considered.
if (this.fragments[^1].EndsWithSoftHyphen) if (state.Fragments[^1].EndsWithSoftHyphen)
xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth; xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth;
// Adjust this fragment's offset from kerning distance. // Adjust this fragment's offset from kerning distance.
xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune); xy.X += state.CalculateScaledDistance(state.Fragments[^1].LastRune, fragment.FirstRune);
fragment.Offset = xy; fragment.Offset = xy;
} }
@ -439,7 +469,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
w = Math.Max(w, xy.X + fragment.VisibleWidth); w = Math.Max(w, xy.X + fragment.VisibleWidth);
xy.X += fragment.AdvanceWidth; xy.X += fragment.AdvanceWidth;
prev = fragment.To; prev = fragment.To;
this.fragments.Add(fragment); state.Fragments.Add(fragment);
if (fragment.BreakAfter) if (fragment.BreakAfter)
{ {
@ -491,7 +521,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (gfdTextureSrv != 0) if (gfdTextureSrv != 0)
{ {
state.Draw( state.Draw(
new ImTextureID(gfdTextureSrv), new(gfdTextureSrv),
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)), offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size, size,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0, useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
@ -528,7 +558,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return; return;
static nint GetGfdTextureSrv() static unsafe nint GetGfdTextureSrv()
{ {
var uim = UIModule.Instance(); var uim = UIModule.Instance();
if (uim is null) if (uim is null)
@ -553,7 +583,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Determines a bitmap icon to display for the given SeString payload.</summary> /// <summary>Determines a bitmap icon to display for the given SeString payload.</summary>
/// <param name="sss">Byte span that should include a SeString payload.</param> /// <param name="sss">Byte span that should include a SeString payload.</param>
/// <returns>Icon to display, or <see cref="None"/> if it should not be displayed as an icon.</returns> /// <returns>Icon to display, or <see cref="None"/> if it should not be displayed as an icon.</returns>
private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss) private unsafe BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
{ {
var e = new ReadOnlySeStringSpan(sss).GetEnumerator(); var e = new ReadOnlySeStringSpan(sss).GetEnumerator();
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2) if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
@ -710,38 +740,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
firstDisplayRune ?? default, firstDisplayRune ?? default,
lastNonSoftHyphenRune); lastNonSoftHyphenRune);
} }
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
private record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}
} }

View file

@ -0,0 +1,39 @@
using System.Numerics;
using System.Text;
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
internal record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{
/// <summary>Gets a value indicating whether the fragment ends with a visible soft hyphen.</summary>
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}

View file

@ -12,7 +12,11 @@ public record struct SeStringDrawParams
/// <summary>Gets or sets the target draw list.</summary> /// <summary>Gets or sets the target draw list.</summary>
/// <value>Target draw list, <c>default(ImDrawListPtr)</c> to not draw, or <c>null</c> to use /// <value>Target draw list, <c>default(ImDrawListPtr)</c> to not draw, or <c>null</c> to use
/// <see cref="ImGui.GetWindowDrawList"/> (the default).</value> /// <see cref="ImGui.GetWindowDrawList"/> (the default).</value>
/// <remarks>If this value is set, <see cref="ImGui.Dummy"/> will not be called, and ImGui ID will be ignored. /// <remarks>
/// 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, 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.
/// Must be set when drawing off the main thread.
/// </remarks> /// </remarks>
public ImDrawListPtr? TargetDrawList { get; set; } public ImDrawListPtr? TargetDrawList { get; set; }
@ -21,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

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -6,6 +7,8 @@ using System.Text;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@ -19,46 +22,88 @@ public unsafe ref struct SeStringDrawState
private static readonly int ChannelCount = Enum.GetValues<SeStringDrawChannel>().Length; private static readonly int ChannelCount = Enum.GetValues<SeStringDrawChannel>().Length;
private readonly ImDrawList* drawList; private readonly ImDrawList* drawList;
private readonly SeStringColorStackSet colorStackSet;
private readonly ImDrawListSplitter* splitter; private ImDrawListSplitter splitter;
/// <summary>Initializes a new instance of the <see cref="SeStringDrawState"/> struct.</summary> /// <summary>Initializes a new instance of the <see cref="SeStringDrawState"/> struct.</summary>
/// <param name="span">Raw SeString byte span.</param> /// <param name="span">Raw SeString byte span.</param>
/// <param name="ssdp">Instance of <see cref="SeStringDrawParams"/> to initialize from.</param> /// <param name="ssdp">Instance of <see cref="SeStringDrawParams"/> to initialize from.</param>
/// <param name="colorStackSet">Instance of <see cref="SeStringColorStackSet"/> to use.</param> /// <param name="colorStackSet">Instance of <see cref="SeStringColorStackSet"/> to use.</param>
/// <param name="splitter">Instance of ImGui Splitter to use.</param> /// <param name="fragments">Fragments.</param>
/// <param name="font">Font to use.</param>
internal SeStringDrawState( internal SeStringDrawState(
ReadOnlySpan<byte> span, ReadOnlySpan<byte> span,
scoped in SeStringDrawParams ssdp, scoped in SeStringDrawParams ssdp,
SeStringColorStackSet colorStackSet, SeStringColorStackSet colorStackSet,
ImDrawListSplitter* splitter) List<TextFragment> fragments,
ImFont* font)
{ {
this.colorStackSet = colorStackSet;
this.splitter = splitter;
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.Span = span; this.Span = span;
this.ColorStackSet = colorStackSet;
this.Fragments = fragments;
this.Font = font;
if (ssdp.TargetDrawList is null)
{
if (!ThreadSafety.IsMainThread)
{
throw new ArgumentException(
$"{nameof(ssdp.TargetDrawList)} must be set to render outside the main thread.");
}
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
}
else
{
this.drawList = ssdp.TargetDrawList.Value;
this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero;
// 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.Color = ssdp.Color ?? uint.MaxValue;
this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.
this.LinkActiveBackColor = 0; // Interactivity is unused outside the main thread.
this.ThemeIndex = ssdp.ThemeIndex ?? 0;
}
this.splitter = default;
this.GetEntity = ssdp.GetEntity; this.GetEntity = ssdp.GetEntity;
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y)); this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
this.Font = ssdp.EffectiveFont; this.FontSizeScale = this.FontSize / this.Font.FontSize;
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
this.FontSizeScale = this.FontSize / this.Font->FontSize;
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight); this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f; this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
this.Opacity = ssdp.EffectiveOpacity; this.Opacity = ssdp.EffectiveOpacity;
this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity; this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000; this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000; this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ForceEdgeColor = ssdp.ForceEdgeColor; this.ForceEdgeColor = ssdp.ForceEdgeColor;
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
this.Bold = ssdp.Bold; this.Bold = ssdp.Bold;
this.Italic = ssdp.Italic; this.Italic = ssdp.Italic;
this.Edge = ssdp.Edge; this.Edge = ssdp.Edge;
this.Shadow = ssdp.Shadow; this.Shadow = ssdp.Shadow;
this.ColorStackSet.Initialize(ref this);
fragments.Clear();
} }
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/> /// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
@ -74,7 +119,7 @@ public unsafe ref struct SeStringDrawState
public Vector2 ScreenOffset { get; } public Vector2 ScreenOffset { get; }
/// <inheritdoc cref="SeStringDrawParams.Font"/> /// <inheritdoc cref="SeStringDrawParams.Font"/>
public ImFont* Font { get; } public ImFontPtr Font { get; }
/// <inheritdoc cref="SeStringDrawParams.FontSize"/> /// <inheritdoc cref="SeStringDrawParams.FontSize"/>
public float FontSize { get; } public float FontSize { get; }
@ -135,7 +180,7 @@ public unsafe ref struct SeStringDrawState
/// <summary>Gets a value indicating whether the edge should be drawn.</summary> /// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawEdge => public readonly bool ShouldDrawEdge =>
(this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000; (this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
/// <summary>Gets a value indicating whether the edge should be drawn.</summary> /// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 }; public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
@ -143,11 +188,17 @@ public unsafe ref struct SeStringDrawState
/// <summary>Gets a value indicating whether the edge should be drawn.</summary> /// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 }; public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
/// <summary>Gets the color stacks.</summary>
internal SeStringColorStackSet ColorStackSet { get; }
/// <summary>Gets the text fragments.</summary>
internal List<TextFragment> Fragments { get; }
/// <summary>Sets the current channel in the ImGui draw list splitter.</summary> /// <summary>Sets the current channel in the ImGui draw list splitter.</summary>
/// <param name="channelIndex">Channel to switch to.</param> /// <param name="channelIndex">Channel to switch to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) => public void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
this.splitter->SetCurrentChannel(this.drawList, (int)channelIndex); this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex);
/// <summary>Draws a single texture.</summary> /// <summary>Draws a single texture.</summary>
/// <param name="igTextureId">ImGui texture ID to draw from.</param> /// <param name="igTextureId">ImGui texture ID to draw from.</param>
@ -216,9 +267,9 @@ public unsafe ref struct SeStringDrawState
/// <summary>Draws a single glyph using current styling configurations.</summary> /// <summary>Draws a single glyph using current styling configurations.</summary>
/// <param name="g">Glyph to draw.</param> /// <param name="g">Glyph to draw.</param>
/// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param> /// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param>
internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset) internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
{ {
var texId = this.Font->ContainerAtlas->Textures.Ref<ImFontAtlasTexture>(g.TextureIndex).TexID; var texId = this.Font.ContainerAtlas.Textures.Ref<ImFontAtlasTexture>(g.TextureIndex).TexID;
var xy0 = new Vector2( var xy0 = new Vector2(
MathF.Round(g.X0 * this.FontSizeScale), MathF.Round(g.X0 * this.FontSizeScale),
MathF.Round(g.Y0 * this.FontSizeScale)); MathF.Round(g.Y0 * this.FontSizeScale));
@ -268,14 +319,14 @@ public unsafe ref struct SeStringDrawState
/// <param name="offset">Offset of the glyph in pixels w.r.t. /// <param name="offset">Offset of the glyph in pixels w.r.t.
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</param> /// <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="advanceWidth">Advance width of the glyph.</param> /// <param name="advanceWidth">Advance width of the glyph.</param>
internal readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth) internal void DrawLinkUnderline(Vector2 offset, float advanceWidth)
{ {
if (this.LinkUnderlineThickness < 1f) if (this.LinkUnderlineThickness < 1f)
return; return;
offset += this.ScreenOffset; offset += this.ScreenOffset;
offset.Y += (this.LinkUnderlineThickness - 1) / 2f; offset.Y += (this.LinkUnderlineThickness - 1) / 2f;
offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font->Ascent * this.FontSizeScale)); offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font.Ascent * this.FontSizeScale));
this.SetCurrentChannel(SeStringDrawChannel.Foreground); this.SetCurrentChannel(SeStringDrawChannel.Foreground);
this.DrawList.AddLine( this.DrawList.AddLine(
@ -302,9 +353,9 @@ public unsafe ref struct SeStringDrawState
internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune) internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
{ {
var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
? this.Font->FindGlyph((ushort)rune.Value) ? (ImFontGlyphPtr)this.Font.FindGlyph((ushort)rune.Value)
: this.Font->FallbackGlyph; : this.Font.FallbackGlyph;
return ref *(ImGuiHelpers.ImFontGlyphReal*)p; return ref *(ImGuiHelpers.ImFontGlyphReal*)p.Handle;
} }
/// <summary>Gets the glyph corresponding to the given codepoint.</summary> /// <summary>Gets the glyph corresponding to the given codepoint.</summary>
@ -337,7 +388,7 @@ public unsafe ref struct SeStringDrawState
return 0; return 0;
return MathF.Round( return MathF.Round(
this.Font->GetDistanceAdjustmentForPair( this.Font.GetDistanceAdjustmentForPair(
(ushort)left.Value, (ushort)left.Value,
(ushort)right.Value) * this.FontSizeScale); (ushort)right.Value) * this.FontSizeScale);
} }
@ -350,15 +401,15 @@ public unsafe ref struct SeStringDrawState
switch (payload.MacroCode) switch (payload.MacroCode)
{ {
case MacroCode.Color: case MacroCode.Color:
this.colorStackSet.HandleColorPayload(ref this, payload); this.ColorStackSet.HandleColorPayload(ref this, payload);
return true; return true;
case MacroCode.EdgeColor: case MacroCode.EdgeColor:
this.colorStackSet.HandleEdgeColorPayload(ref this, payload); this.ColorStackSet.HandleEdgeColorPayload(ref this, payload);
return true; return true;
case MacroCode.ShadowColor: case MacroCode.ShadowColor:
this.colorStackSet.HandleShadowColorPayload(ref this, payload); this.ColorStackSet.HandleShadowColorPayload(ref this, payload);
return true; return true;
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u): case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
@ -379,11 +430,11 @@ public unsafe ref struct SeStringDrawState
return true; return true;
case MacroCode.ColorType: case MacroCode.ColorType:
this.colorStackSet.HandleColorTypePayload(ref this, payload); this.ColorStackSet.HandleColorTypePayload(ref this, payload);
return true; return true;
case MacroCode.EdgeColorType: case MacroCode.EdgeColorType:
this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload); this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload);
return true; return true;
default: default:
@ -393,10 +444,9 @@ public unsafe ref struct SeStringDrawState
/// <summary>Splits the draw list.</summary> /// <summary>Splits the draw list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly void SplitDrawList() => internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount);
this.splitter->Split(this.drawList, ChannelCount);
/// <summary>Merges the draw list.</summary> /// <summary>Merges the draw list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList); internal void MergeDrawList() => this.splitter.Merge(this.drawList);
} }

View file

@ -533,6 +533,13 @@ internal class DalamudInterface : IInternalDisposableService
this.creditsDarkeningAnimation.Restart(); this.creditsDarkeningAnimation.Restart();
} }
/// <inheritdoc cref="DataWindow.GetWidget{T}"/>
public T GetDataWindowWidget<T>() where T : IDataWindowWidget => this.dataWindow.GetWidget<T>();
/// <summary>Sets the data window current widget.</summary>
/// <param name="widget">Widget to set current.</param>
public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget;
private void OnDraw() private void OnDraw()
{ {
this.FrameCount++; this.FrameCount++;
@ -660,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();
@ -832,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

@ -256,7 +256,7 @@ internal partial class InterfaceManager : IInternalDisposableService
var gwh = default(HWND); var gwh = default(HWND);
fixed (char* pClass = "FFXIVGAME") fixed (char* pClass = "FFXIVGAME")
{ {
while ((gwh = FindWindowExW(default, gwh, (ushort*)pClass, default)) != default) while ((gwh = FindWindowExW(default, gwh, pClass, default)) != default)
{ {
uint pid; uint pid;
_ = GetWindowThreadProcessId(gwh, &pid); _ = GetWindowThreadProcessId(gwh, &pid);

View file

@ -63,11 +63,11 @@ internal sealed unsafe partial class ReShadeAddonInterface
return; return;
bool GetProcAddressInto(ProcessModule m, ReadOnlySpan<char> name, void* res) static bool GetProcAddressInto(ProcessModule m, ReadOnlySpan<char> name, void* res)
{ {
Span<byte> name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1]; Span<byte> name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1];
name8[Encoding.UTF8.GetBytes(name, name8)] = 0; name8[Encoding.UTF8.GetBytes(name, name8)] = 0;
*(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0])); *(nint*)res = (nint)GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0]));
return *(nint*)res != 0; return *(nint*)res != 0;
} }
} }
@ -174,7 +174,7 @@ internal sealed unsafe partial class ReShadeAddonInterface
CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE, CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE,
CERT.CERT_NAME_ISSUER_FLAG, CERT.CERT_NAME_ISSUER_FLAG,
null, null,
(ushort*)Unsafe.AsPointer(ref issuerName[0]), (char*)Unsafe.AsPointer(ref issuerName[0]),
pcb); pcb);
if (pcb == 0) if (pcb == 0)
throw new Win32Exception("CertGetNameStringW(2)"); throw new Win32Exception("CertGetNameStringW(2)");

View file

@ -94,7 +94,7 @@ internal static unsafe class ReShadeUnwrapper
static bool HasProcExported(ProcessModule m, ReadOnlySpan<byte> name) static bool HasProcExported(ProcessModule m, ReadOnlySpan<byte> name)
{ {
fixed (byte* p = name) fixed (byte* p = name)
return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0; return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != null;
} }
} }

View file

@ -216,7 +216,7 @@ internal partial class StaThreadService : IInternalDisposableService
lpfnWndProc = &MessageReceiverWndProcStatic, lpfnWndProc = &MessageReceiverWndProcStatic,
hInstance = hInstance, hInstance = hInstance,
hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1), hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1),
lpszClassName = (ushort*)name, lpszClassName = name,
}; };
wndClassAtom = RegisterClassExW(&wndClass); wndClassAtom = RegisterClassExW(&wndClass);
@ -226,8 +226,8 @@ internal partial class StaThreadService : IInternalDisposableService
this.messageReceiverHwndTask.SetResult( this.messageReceiverHwndTask.SetResult(
CreateWindowExW( CreateWindowExW(
0, 0,
(ushort*)wndClassAtom, (char*)wndClassAtom,
(ushort*)name, name,
0, 0,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
@ -275,7 +275,7 @@ internal partial class StaThreadService : IInternalDisposableService
_ = OleFlushClipboard(); _ = OleFlushClipboard();
OleUninitialize(); OleUninitialize();
if (wndClassAtom != 0) if (wndClassAtom != 0)
UnregisterClassW((ushort*)wndClassAtom, hInstance); UnregisterClassW((char*)wndClassAtom, hInstance);
this.messageReceiverHwndTask.TrySetException(e); this.messageReceiverHwndTask.TrySetException(e);
} }
} }

View file

@ -68,7 +68,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept; private bool isExcept;
private bool selectionCollapsed; private bool selectionCollapsed;
private IDataWindowWidget currentWidget;
private bool isLoaded; private bool isLoaded;
/// <summary> /// <summary>
@ -82,9 +82,12 @@ internal class DataWindow : Window, IDisposable
this.RespectCloseHotkey = false; this.RespectCloseHotkey = false;
this.orderedModules = this.modules.OrderBy(module => module.DisplayName); this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
this.currentWidget = this.orderedModules.First(); this.CurrentWidget = this.orderedModules.First();
} }
/// <summary>Gets or sets the current widget.</summary>
public IDataWindowWidget CurrentWidget { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() => this.modules.OfType<IDisposable>().AggregateToDisposable().Dispose(); public void Dispose() => this.modules.OfType<IDisposable>().AggregateToDisposable().Dispose();
@ -99,6 +102,20 @@ internal class DataWindow : Window, IDisposable
{ {
} }
/// <summary>Gets the data window widget of the specified type.</summary>
/// <typeparam name="T">Type of the data window widget to find.</typeparam>
/// <returns>Found widget.</returns>
public T GetWidget<T>() where T : IDataWindowWidget
{
foreach (var m in this.modules)
{
if (m is T w)
return w;
}
throw new ArgumentException($"No widget of type {typeof(T).FullName} found.");
}
/// <summary> /// <summary>
/// Set the DataKind dropdown menu. /// Set the DataKind dropdown menu.
/// </summary> /// </summary>
@ -110,7 +127,7 @@ internal class DataWindow : Window, IDisposable
if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule) if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule)
{ {
this.currentWidget = targetModule; this.CurrentWidget = targetModule;
} }
else else
{ {
@ -153,9 +170,9 @@ internal class DataWindow : Window, IDisposable
{ {
foreach (var widget in this.orderedModules) foreach (var widget in this.orderedModules)
{ {
if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget)) if (ImGui.Selectable(widget.DisplayName, this.CurrentWidget == widget))
{ {
this.currentWidget = widget; this.CurrentWidget = widget;
} }
} }
@ -206,9 +223,9 @@ internal class DataWindow : Window, IDisposable
try try
{ {
if (this.currentWidget is { Ready: true }) if (this.CurrentWidget is { Ready: true })
{ {
this.currentWidget.Draw(); this.CurrentWidget.Draw();
} }
else else
{ {

View file

@ -1,9 +1,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
using Lumina.Text.ReadOnly;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -87,12 +93,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10f); ImGuiHelpers.ScaledDummy(10f);
for (var i = 0; i < this.icons?.Count; i++) for (var i = 0; i < this.icons?.Count; i++)
{ {
if (this.icons[i] == FontAwesomeIcon.None)
continue;
ImGui.AlignTextToFramePadding();
ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}"); ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}");
ImGuiHelpers.ScaledRelativeSameLine(50f); ImGuiHelpers.ScaledRelativeSameLine(50f);
ImGui.Text($"{this.iconNames?[i]}"); ImGui.Text($"{this.iconNames?[i]}");
ImGuiHelpers.ScaledRelativeSameLine(280f); ImGuiHelpers.ScaledRelativeSameLine(280f);
ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont); ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
ImGui.Text(this.icons[i].ToIconString()); ImGui.Text(this.icons[i].ToIconString());
ImGuiHelpers.ScaledRelativeSameLine(320f);
if (this.useFixedWidth
? ImGui.Button($"{(char)this.icons[i]}##FontAwesomeIconButton{i}")
: ImGuiComponents.IconButton($"##FontAwesomeIconButton{i}", this.icons[i]))
{
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
this.DisplayName,
this.icons[i].ToString(),
Task.FromResult(
Service<TextureManager>.Get().CreateTextureFromSeString(
ReadOnlySeString.FromText(this.icons[i].ToIconString()),
new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() })));
}
ImGui.PopFont(); ImGui.PopFont();
ImGuiHelpers.ScaledDummy(2f); ImGuiHelpers.ScaledDummy(2f);
} }

View file

@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Data; using Dalamud.Data;
@ -9,11 +10,13 @@ using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
using Dalamud.Storage.Assets; using Dalamud.Storage.Assets;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Lumina.Text; using Lumina.Text;
using Lumina.Text.Parse;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@ -56,11 +59,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public void Draw() public void Draw()
{ {
var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ?? ImGui.GetColorU32(ImGuiCol.Text)); var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ??= ImGui.GetColorU32(ImGuiCol.Text));
if (ImGui.ColorEdit4("Color", ref t2)) if (ImGui.ColorEdit4("Color", ref t2))
this.style.Color = ImGui.ColorConvertFloat4ToU32(t2); this.style.Color = ImGui.ColorConvertFloat4ToU32(t2);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ?? 0xFF000000u); t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ??= 0xFF000000u);
if (ImGui.ColorEdit4("Edge Color", ref t2)) if (ImGui.ColorEdit4("Edge Color", ref t2))
this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2); this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2);
@ -69,27 +72,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.Checkbox("Forced"u8, ref t)) if (ImGui.Checkbox("Forced"u8, ref t))
this.style.ForceEdgeColor = t; this.style.ForceEdgeColor = t;
t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ?? 0xFF000000u); t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ??= 0xFF000000u);
if (ImGui.ColorEdit4("Shadow Color", ref t2)) if (ImGui.ColorEdit4("Shadow Color"u8, ref t2))
this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2); this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered)); t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonHovered));
if (ImGui.ColorEdit4("Link Hover Color", ref t2)) if (ImGui.ColorEdit4("Link Hover Color"u8, ref t2))
this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2); this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive)); t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonActive));
if (ImGui.ColorEdit4("Link Active Color", ref t2)) if (ImGui.ColorEdit4("Link Active Color"u8, ref t2))
this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2); this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2);
var t3 = this.style.LineHeight ?? 1f; var t3 = this.style.LineHeight ??= 1f;
if (ImGui.DragFloat("Line Height"u8, ref t3, 0.01f, 0.4f, 3f, "%.02f")) if (ImGui.DragFloat("Line Height"u8, ref t3, 0.01f, 0.4f, 3f, "%.02f"))
this.style.LineHeight = t3; this.style.LineHeight = t3;
t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha; t3 = this.style.Opacity ??= 1f;
if (ImGui.DragFloat("Opacity"u8, ref t3, 0.005f, 0f, 1f, "%.02f")) if (ImGui.DragFloat("Opacity"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.Opacity = t3; this.style.Opacity = t3;
t3 = this.style.EdgeStrength ?? 0.25f; t3 = this.style.EdgeStrength ??= 0.25f;
if (ImGui.DragFloat("Edge Strength"u8, ref t3, 0.005f, 0f, 1f, "%.02f")) if (ImGui.DragFloat("Edge Strength"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.EdgeStrength = t3; this.style.EdgeStrength = t3;
@ -174,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))
@ -240,6 +261,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
Service<SeStringRenderer>.Get().CompileAndCache(this.testString).Data.Span)); Service<SeStringRenderer>.Get().CompileAndCache(this.testString).Data.Span));
} }
ImGui.SameLine();
if (ImGui.Button("Copy as Image"))
{
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
this.DisplayName,
$"From {nameof(SeStringRendererTestWidget)}",
Task.FromResult(
Service<TextureManager>.Get().CreateTextureFromSeString(
ReadOnlySeString.FromMacroString(
this.testString,
new(ExceptionMode: MacroStringParseExceptionMode.EmbedError)),
this.style with
{
Font = ImGui.GetFont(),
FontSize = ImGui.GetFontSize(),
WrapWidth = ImGui.GetContentRegionAvail().X,
ThemeIndex = AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType,
})));
}
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
ImGuiHelpers.CompileSeStringWrapped( ImGuiHelpers.CompileSeStringWrapped(
"Optional features implemented for the following test input:<br>" + "Optional features implemented for the following test input:<br>" +

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@ -306,12 +306,12 @@ internal class TexWidget : IDataWindowWidget
pres->Release(); pres->Release();
ImGui.Text($"RC: Resource({rcres})/View({rcsrv})"); ImGui.Text($"RC: Resource({rcres})/View({rcsrv})");
ImGui.Text(source.ToString()); ImGui.Text($"{source.Width} x {source.Height} | {source}");
} }
else else
{ {
ImGui.Text("RC: -"u8); ImGui.Text("RC: -");
ImGui.Text(" "u8); ImGui.Text(string.Empty);
} }
} }
@ -342,6 +342,10 @@ internal class TexWidget : IDataWindowWidget
runLater?.Invoke(); runLater?.Invoke();
} }
/// <summary>Adds a texture wrap for debug display purposes.</summary>
/// <param name="textureTask">Task returning a texture.</param>
public void AddTexture(Task<IDalamudTextureWrap> textureTask) => this.addedTextures.Add(new(Api10: textureTask));
private unsafe void DrawBlame(List<TextureManager.IBlameableDalamudTextureWrap> allBlames) private unsafe void DrawBlame(List<TextureManager.IBlameableDalamudTextureWrap> allBlames)
{ {
var im = Service<InterfaceManager>.Get(); var im = Service<InterfaceManager>.Get();

View file

@ -191,6 +191,29 @@ internal class NounProcessorSelfTestStep : ISelfTestStep
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"),
// ColumnOffset tests
new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a Amalj'aa"),
new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Amalj'aa"),
new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an aetherpool arm"),
new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the aetherpool arm"),
new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pomander of safety"),
new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pomander of safety"),
new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a splinter of Inferno magicite"),
new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the splinter of Inferno magicite"),
new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an Unei demiclone"),
new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Unei demiclone"),
new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pair of oval spectacles"),
new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pair of oval spectacles"),
new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a shaded spectacles"),
new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the shaded spectacles"),
]; ];
private enum GermanCases private enum GermanCases

View file

@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
: base( : base(
"TitleScreenMenuOverlay", "TitleScreenMenuOverlay",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus |
ImGuiWindowFlags.NoDocking)
{ {
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true); this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
@ -33,10 +34,22 @@ public interface IFontHandle : IDisposable
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br /> /// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br />
/// Alternatively, use <see cref="WaitAsync"/> to wait for this property to become <c>true</c>. /// Alternatively, use <see cref="WaitAsync()"/> to wait for this property to become <c>true</c>.
/// </remarks> /// </remarks>
bool Available { get; } bool Available { get; }
/// <summary>
/// Attempts to lock the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
/// <see cref="IFontHandle"/>, for use in any thread.<br />
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
/// </summary>
/// <param name="errorMessage">The error message, if any.</param>
/// <returns>
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
/// </returns>
ILockedImFont? TryLock(out string? errorMessage);
/// <summary> /// <summary>
/// Locks the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this /// Locks the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
/// <see cref="IFontHandle"/>, for use in any thread.<br /> /// <see cref="IFontHandle"/>, for use in any thread.<br />
@ -92,4 +105,11 @@ public interface IFontHandle : IDisposable
/// </summary> /// </summary>
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns> /// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
Task<IFontHandle> WaitAsync(); Task<IFontHandle> WaitAsync();
/// <summary>
/// Waits for <see cref="Available"/> to become <c>true</c>.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
Task<IFontHandle> WaitAsync(CancellationToken cancellationToken);
} }

View file

@ -15,7 +15,6 @@ using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets; using Dalamud.Storage.Assets;
using Dalamud.Utility; using Dalamud.Utility;
using SharpDX.DXGI;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas.Internals; namespace Dalamud.Interface.ManagedFontAtlas.Internals;
@ -749,7 +748,7 @@ internal sealed partial class FontAtlasFactory
new( new(
width, width,
height, height,
(int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), (int)(use4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM),
width * bpp), width * bpp),
buf, buf,
name); name);

View file

@ -238,12 +238,17 @@ internal abstract class FontHandle : IFontHandle
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task<IFontHandle> WaitAsync() public Task<IFontHandle> WaitAsync() => this.WaitAsync(CancellationToken.None);
/// <inheritdoc/>
public Task<IFontHandle> WaitAsync(CancellationToken cancellationToken)
{ {
if (this.Available) if (this.Available)
return Task.FromResult<IFontHandle>(this); return Task.FromResult<IFontHandle>(this);
var tcs = new TaskCompletionSource<IFontHandle>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<IFontHandle>(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => tcs.TrySetCanceled());
this.ImFontChanged += OnImFontChanged; this.ImFontChanged += OnImFontChanged;
this.Disposed += OnDisposed; this.Disposed += OnDisposed;
if (this.Available) if (this.Available)

View file

@ -44,12 +44,12 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo
private static unsafe string ReadStringUsing( private static unsafe string ReadStringUsing(
IWICBitmapCodecInfo* codecInfo, IWICBitmapCodecInfo* codecInfo,
delegate* unmanaged<IWICBitmapCodecInfo*, uint, ushort*, uint*, int> readFuncPtr) delegate* unmanaged[MemberFunction]<IWICBitmapCodecInfo*, uint, char*, uint*, int> readFuncPtr)
{ {
var cch = 0u; var cch = 0u;
_ = readFuncPtr(codecInfo, 0, null, &cch); _ = readFuncPtr(codecInfo, 0, null, &cch);
var buf = stackalloc char[(int)cch + 1]; var buf = stackalloc char[(int)cch + 1];
Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, (ushort*)buf, &cch)); Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch));
return new(buf, 0, (int)cch); return new(buf, 0, (int)cch);
} }
} }

View file

@ -219,14 +219,14 @@ internal sealed partial class TextureManager
return; return;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) => static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) =>
ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0);
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0);
} }

View file

@ -133,7 +133,7 @@ internal sealed partial class TextureManager
}, },
}, },
}; };
namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260)); namea.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgda.fgd.e0.cFileName[0]), 260));
AddToDataObject( AddToDataObject(
pdo, pdo,
@ -157,7 +157,7 @@ internal sealed partial class TextureManager
}, },
}, },
}; };
preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260)); preferredFileNameWithoutExtension.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgdw.fgd.e0.cFileName[0]), 260));
AddToDataObject( AddToDataObject(
pdo, pdo,
@ -450,7 +450,7 @@ internal sealed partial class TextureManager
try try
{ {
IStream* pfs; IStream* pfs;
SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError(); SHCreateStreamOnFileW((char*)pPath, sharedRead, &pfs).ThrowOnError();
var stgm2 = new STGMEDIUM var stgm2 = new STGMEDIUM
{ {

View file

@ -0,0 +1,35 @@
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Utility;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
[ServiceManager.ServiceDependency]
private readonly SeStringRenderer seStringRenderer = Service<SeStringRenderer>.Get();
/// <inheritdoc/>
public IDalamudTextureWrap CreateTextureFromSeString(
ReadOnlySpan<byte> text,
scoped in SeStringDrawParams drawParams = default,
string? debugName = null)
{
ThreadSafety.AssertMainThread();
using var dd = this.seStringRenderer.CreateDrawData(text, drawParams);
var texture = this.CreateDrawListTexture(debugName ?? nameof(this.CreateTextureFromSeString));
try
{
texture.Size = dd.Data.DisplaySize;
texture.Draw(dd.DataPtr);
return texture;
}
catch
{
texture.Dispose();
throw;
}
}
}

View file

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
@ -248,7 +249,7 @@ internal sealed partial class TextureManager
usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC; usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
else else
usage = D3D11_USAGE.D3D11_USAGE_DEFAULT; usage = D3D11_USAGE.D3D11_USAGE_DEFAULT;
using var texture = this.device.CreateTexture2D( using var texture = this.device.CreateTexture2D(
new() new()
{ {

View file

@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.IoC; using Dalamud.IoC;
@ -283,6 +284,18 @@ internal sealed class TextureManagerPluginScoped
return textureWrap; return textureWrap;
} }
/// <inheritdoc/>
public IDalamudTextureWrap CreateTextureFromSeString(
ReadOnlySpan<byte> text,
scoped in SeStringDrawParams drawParams = default,
string? debugName = null)
{
var manager = this.ManagerOrThrow;
var textureWrap = manager.CreateTextureFromSeString(text, drawParams, debugName);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() => public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
@ -12,7 +13,6 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog; using Serilog;
@ -150,13 +150,6 @@ public interface IUiBuilder
/// </summary> /// </summary>
public ImFontPtr FontMono { get; } public ImFontPtr FontMono { get; }
/// <summary>
/// Gets the game's active Direct3D device.
/// </summary>
// TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud.
[Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")]
SharpDX.Direct3D11.Device Device { get; }
/// <summary>Gets the game's active Direct3D device.</summary> /// <summary>Gets the game's active Direct3D device.</summary>
/// <value>Pointer to the instance of IUnknown that the game is using and should be containing an ID3D11Device, /// <value>Pointer to the instance of IUnknown that the game is using and should be containing an ID3D11Device,
/// or 0 if it is not available yet.</value> /// or 0 if it is not available yet.</value>
@ -226,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>
@ -302,8 +301,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
private IFontHandle? monoFontHandle; private IFontHandle? monoFontHandle;
private IFontHandle? iconFontFixedWidthHandle; private IFontHandle? iconFontFixedWidthHandle;
private SharpDX.Direct3D11.Device? sdxDevice;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it. /// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
/// You do not have to call this manually. /// You do not have to call this manually.
@ -493,12 +490,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
this.InterfaceManagerWithScene?.MonoFontHandle this.InterfaceManagerWithScene?.MonoFontHandle
?? throw new InvalidOperationException("Scene is not yet ready."))); ?? throw new InvalidOperationException("Scene is not yet ready.")));
/// <inheritdoc/>
// TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud.
[Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")]
public SharpDX.Direct3D11.Device Device =>
this.sdxDevice ??= new(this.InterfaceManagerWithScene!.Backend!.DeviceHandle);
/// <inheritdoc/> /// <inheritdoc/>
public nint DeviceHandle => this.InterfaceManagerWithScene?.Backend?.DeviceHandle ?? 0; public nint DeviceHandle => this.InterfaceManagerWithScene?.Backend?.DeviceHandle ?? 0;
@ -575,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>
@ -691,13 +685,14 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
FontAtlasAutoRebuildMode autoRebuildMode, FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true, bool isGlobalScaled = true,
string? debugName = null) => string? debugName = null) =>
this.scopedFinalizer.Add(Service<FontAtlasFactory> this.scopedFinalizer.Add(
.Get() Service<FontAtlasFactory>
.CreateFontAtlas( .Get()
this.namespaceName + ":" + (debugName ?? "custom"), .CreateFontAtlas(
autoRebuildMode, this.namespaceName + ":" + (debugName ?? "custom"),
isGlobalScaled, autoRebuildMode,
this.plugin)); isGlobalScaled,
this.plugin));
/// <summary> /// <summary>
/// Unregister the UiBuilder. Do not call this in plugin code. /// Unregister the UiBuilder. Do not call this in plugin code.
@ -868,6 +863,15 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
// Note: do not dispose w; we do not own it // Note: do not dispose w; we do not own it
} }
public ILockedImFont? TryLock(out string? errorMessage)
{
if (this.wrapped is { } w)
return w.TryLock(out errorMessage);
errorMessage = nameof(ObjectDisposedException);
return null;
}
public ILockedImFont Lock() => public ILockedImFont Lock() =>
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
@ -876,7 +880,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
public void Pop() => this.WrappedNotDisposed.Pop(); public void Pop() => this.WrappedNotDisposed.Pop();
public Task<IFontHandle> WaitAsync() => public Task<IFontHandle> WaitAsync() =>
this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this); this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this)
?? Task.FromException<IFontHandle>(new ObjectDisposedException(nameof(FontHandleWrapper)));
public Task<IFontHandle> WaitAsync(CancellationToken cancellationToken) =>
this.wrapped?.WaitAsync(cancellationToken)
.ContinueWith(_ => (IFontHandle)this, cancellationToken)
?? Task.FromException<IFontHandle>(new ObjectDisposedException(nameof(FontHandleWrapper)));
public override string ToString() => public override string ToString() =>
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})"; $"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";

View file

@ -0,0 +1,92 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.Utility;
/// <summary>Wrapper aroundx <see cref="ImDrawData"/> containing one <see cref="ImDrawList"/>.</summary>
public unsafe struct BufferBackedImDrawData : IDisposable
{
private nint buffer;
/// <summary>Initializes a new instance of the <see cref="BufferBackedImDrawData"/> struct.</summary>
/// <param name="buffer">Address of buffer to use.</param>
private BufferBackedImDrawData(nint buffer) => this.buffer = buffer;
/// <summary>Gets the <see cref="ImDrawData"/> stored in this buffer.</summary>
public readonly ref ImDrawData Data => ref ((DataStruct*)this.buffer)->Data;
/// <summary>Gets the <see cref="ImDrawDataPtr"/> stored in this buffer.</summary>
public readonly ImDrawDataPtr DataPtr => new((ImDrawData*)Unsafe.AsPointer(ref this.Data));
/// <summary>Gets the <see cref="ImDrawList"/> stored in this buffer.</summary>
public readonly ref ImDrawList List => ref ((DataStruct*)this.buffer)->List;
/// <summary>Gets the <see cref="ImDrawListPtr"/> stored in this buffer.</summary>
public readonly ImDrawListPtr ListPtr => new((ImDrawList*)Unsafe.AsPointer(ref this.List));
/// <summary>Creates a new instance of <see cref="BufferBackedImDrawData"/>.</summary>
/// <returns>A new instance of <see cref="BufferBackedImDrawData"/>.</returns>
public static BufferBackedImDrawData Create()
{
if (ImGui.GetCurrentContext().IsNull || ImGui.GetIO().FontDefault.Handle is null)
throw new("ImGui is not ready");
var res = new BufferBackedImDrawData(Marshal.AllocHGlobal(sizeof(DataStruct)));
var ds = (DataStruct*)res.buffer;
*ds = default;
var atlas = ImGui.GetIO().Fonts;
ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID;
ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel;
ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]);
ds->SharedData.Font = ImGui.GetIO().FontDefault;
ds->SharedData.FontSize = ds->SharedData.Font->FontSize;
ds->SharedData.ClipRectFullscreen = new(
float.NegativeInfinity,
float.NegativeInfinity,
float.PositiveInfinity,
float.PositiveInfinity);
ds->List.Data = &ds->SharedData;
ds->ListPtr = &ds->List;
ds->Data.CmdLists = &ds->ListPtr;
ds->Data.CmdListsCount = 1;
ds->Data.FramebufferScale = Vector2.One;
res.ListPtr._ResetForNewFrame();
res.ListPtr.PushClipRectFullScreen();
res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon));
return res;
}
/// <summary>Updates the statistics information stored in <see cref="DataPtr"/> from <see cref="ListPtr"/>.</summary>
public readonly void UpdateDrawDataStatistics()
{
this.Data.TotalIdxCount = this.List.IdxBuffer.Size;
this.Data.TotalVtxCount = this.List.VtxBuffer.Size;
}
/// <inheritdoc/>
public void Dispose()
{
if (this.buffer != 0)
{
this.ListPtr._ClearFreeMemory();
Marshal.FreeHGlobal(this.buffer);
this.buffer = 0;
}
}
[StructLayout(LayoutKind.Sequential)]
private struct DataStruct
{
public ImDrawData Data;
public ImDrawList* ListPtr;
public ImDrawList List;
public ImDrawListSharedData SharedData;
}
}

View file

@ -575,6 +575,15 @@ public static partial class ImGuiHelpers
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.IsNull ? other : self; self.IsNull ? other : self;
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
/// <param name="sss">SeString to render.</param>
/// <param name="style">Initial rendering style.</param>
/// <returns>A new self-contained draw data.</returns>
internal static BufferBackedImDrawData CreateDrawData(
ReadOnlySpan<byte> sss,
scoped in SeStringDrawParams style = default) =>
Service<SeStringRenderer>.Get().CreateDrawData(sss, style);
/// <summary> /// <summary>
/// Mark 4K page as used, after adding a codepoint to a font. /// Mark 4K page as used, after adding a codepoint to a font.
/// </summary> /// </summary>

View file

@ -6,10 +6,12 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Serilog; using Serilog;
@ -33,6 +35,14 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
this.interfaceManager.Draw += this.InterfaceManagerOnDraw; this.interfaceManager.Draw += this.InterfaceManagerOnDraw;
} }
private enum ContextMenuActionType
{
None,
SaveAsFile,
CopyToClipboard,
SendToTexWidget,
}
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw; void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw;
@ -66,15 +76,16 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
var textureManager = await Service<TextureManager>.GetAsync(); var textureManager = await Service<TextureManager>.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.Handle.Handle:X}"; var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.Handle.Handle:X}";
ContextMenuActionType action;
BitmapCodecInfo? encoder; BitmapCodecInfo? encoder;
{ {
var first = true; var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo?>( var tcs = new TaskCompletionSource<(ContextMenuActionType Action, BitmapCodecInfo? Codec)>(
TaskCreationOptions.RunContinuationsAsynchronously); TaskCreationOptions.RunContinuationsAsynchronously);
Service<InterfaceManager>.Get().Draw += DrawChoices; Service<InterfaceManager>.Get().Draw += DrawChoices;
encoder = await tcs.Task; (action, encoder) = await tcs.Task;
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")] [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")]
void DrawChoices() void DrawChoices()
@ -98,13 +109,20 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
} }
if (ImGui.Selectable("Copy"u8)) if (ImGui.Selectable("Copy"u8))
tcs.TrySetResult(null); tcs.TrySetResult((ContextMenuActionType.CopyToClipboard, null));
if (ImGui.Selectable("Send to TexWidget"u8))
tcs.TrySetResult((ContextMenuActionType.SendToTexWidget, null));
ImGui.Separator();
foreach (var encoder2 in encoders) foreach (var encoder2 in encoders)
{ {
if (ImGui.Selectable(encoder2.Name)) if (ImGui.Selectable(encoder2.Name))
tcs.TrySetResult(encoder2); tcs.TrySetResult((ContextMenuActionType.SaveAsFile, encoder2));
} }
ImGui.Separator();
const float previewImageWidth = 320; const float previewImageWidth = 320;
var size = textureWrap.Size; var size = textureWrap.Size;
if (size.X > previewImageWidth) if (size.X > previewImageWidth)
@ -120,50 +138,68 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
} }
} }
if (encoder is null) switch (action)
{ {
isCopy = true; case ContextMenuActionType.CopyToClipboard:
await textureManager.CopyToClipboardAsync(textureWrap, name, true); isCopy = true;
} await textureManager.CopyToClipboardAsync(textureWrap, name, true);
else break;
{
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); case ContextMenuActionType.SendToTexWidget:
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
name + encoder.Extensions.First(),
encoder.Extensions.First(),
(ok, path2) =>
{
if (!ok)
tcs.SetCanceled();
else
tcs.SetResult(path2);
});
var path = await tcs.Task.ConfigureAwait(false);
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
var notif = Service<NotificationManager>.Get().AddNotification(
new()
{
Content = $"File saved to: {path}",
Title = initiatorName,
Type = NotificationType.Success,
});
notif.Click += n =>
{ {
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); var framework = await Service<Framework>.GetAsync();
n.Notification.DismissNow(); var dalamudInterface = await Service<DalamudInterface>.GetAsync();
}; await framework.RunOnFrameworkThread(
() =>
{
var texWidget = dalamudInterface.GetDataWindowWidget<TexWidget>();
dalamudInterface.SetDataWindowWidget(texWidget);
texWidget.AddTexture(Task.FromResult(textureWrap.CreateWrapSharingLowLevelResource()));
});
break;
}
case ContextMenuActionType.SaveAsFile when encoder is not null:
{
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
name + encoder.Extensions.First(),
encoder.Extensions.First(),
(ok, path2) =>
{
if (!ok)
tcs.SetCanceled();
else
tcs.SetResult(path2);
});
var path = await tcs.Task.ConfigureAwait(false);
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
var notif = Service<NotificationManager>.Get().AddNotification(
new()
{
Content = $"File saved to: {path}",
Title = initiatorName,
Type = NotificationType.Success,
});
notif.Click += n =>
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
n.Notification.DismissNow();
};
break;
}
} }
} }
catch (Exception e) catch (Exception e)

View file

@ -1,9 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing.Persistence; using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Windowing; namespace Dalamud.Interface.Windowing;
/// <summary> /// <summary>
@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing;
public abstract class Window public abstract class Window
{ {
private const float FadeInOutTime = 0.072f; private const float FadeInOutTime = 0.072f;
private const string AdditionsPopupName = "WindowSystemContextActions";
private static readonly ModuleLog Log = new("WindowSystem"); private static readonly ModuleLog Log = new("WindowSystem");
private static bool wasEscPressedLastFrame = false; private static bool wasEscPressedLastFrame = false;
private readonly TitleBarButton additionsButton;
private readonly List<TitleBarButton> allButtons = [];
private bool internalLastIsOpen = false; private bool internalLastIsOpen = false;
private bool internalIsOpen = false; private bool internalIsOpen = false;
private bool internalIsPinned = false; private bool internalIsPinned = false;
@ -72,6 +76,20 @@ public abstract class Window
this.WindowName = name; this.WindowName = name;
this.Flags = flags; this.Flags = flags;
this.ForceMainWindow = forceMainWindow; this.ForceMainWindow = forceMainWindow;
this.additionsButton = new()
{
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(AdditionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
} }
/// <summary> /// <summary>
@ -425,8 +443,17 @@ public abstract class Window
UIGlobals.PlaySoundEffect(this.OnOpenSfxId); UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
} }
this.PreDraw(); var isErrorStylePushed = false;
this.ApplyConditionals(); if (!this.hasError)
{
this.PreDraw();
this.ApplyConditionals();
}
else
{
Style.StyleModelV1.DalamudStandard.Push();
isErrorStylePushed = true;
}
if (this.ForceMainWindow) if (this.ForceMainWindow)
ImGuiHelpers.ForceNextWindowMainViewport(); ImGuiHelpers.ForceNextWindowMainViewport();
@ -448,10 +475,22 @@ public abstract class Window
var flags = this.Flags; var flags = this.Flags;
if (this.internalIsPinned || this.internalIsClickthrough) if (this.internalIsPinned || this.internalIsClickthrough)
{
flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
}
if (this.internalIsClickthrough) if (this.internalIsClickthrough)
{
flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
}
// If we have an error, reset all flags to default, and unlock window size.
if (this.hasError)
{
flags = ImGuiWindowFlags.None;
ImGui.SetNextWindowCollapsed(false, ImGuiCond.Once);
ImGui.SetNextWindowSizeConstraints(Vector2.Zero, Vector2.PositiveInfinity);
}
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
{ {
@ -461,14 +500,12 @@ public abstract class Window
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough; ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
} }
// Not supported yet on non-main viewports if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{ {
this.internalAlpha = null; if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs)
this.internalIsPinned = false; ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs;
this.internalIsClickthrough = false; else
this.presetDirty = true; ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
} }
if (this.hasError) if (this.hasError)
@ -492,7 +529,6 @@ public abstract class Window
} }
} }
const string additionsPopupName = "WindowSystemContextActions";
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) && var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar); !flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) && var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
@ -503,13 +539,8 @@ public abstract class Window
{ {
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove)) if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove))
{ {
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
if (!isAvailable)
ImGui.BeginDisabled();
if (this.internalIsClickthrough) if (this.internalIsClickthrough)
ImGui.BeginDisabled(); ImGui.BeginDisabled();
@ -557,21 +588,11 @@ public abstract class Window
this.presetDirty = true; this.presetDirty = true;
} }
if (isAvailable) ImGui.TextColored(
{ ImGuiColors.DalamudGrey,
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize(
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", "WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again by clicking the three dashes to disable clickthrough.")); "Open this menu again by clicking the three dashes to disable clickthrough."));
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionViewportDisclaimer",
"These features are only available if this window is inside the game window."));
}
if (!isAvailable)
ImGui.EndDisabled();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"))) if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true; printWindow = true;
@ -582,34 +603,15 @@ public abstract class Window
ImGui.PopStyleVar(); ImGui.PopStyleVar();
} }
unsafe if (flagsApplicableForTitleBarIcons)
{ {
var window = ImGuiP.GetCurrentWindow(); this.allButtons.Clear();
this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1);
ImRect outRect; this.allButtons.AddRange(this.TitleBarButtons);
ImGuiP.TitleBarRect(&outRect, window); if (showAdditions)
this.allButtons.Add(this.additionsButton);
var additionsButton = new TitleBarButton this.allButtons.Sort(static (a, b) => b.Priority - a.Priority);
{ this.DrawTitleBarButtons();
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(additionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
if (flagsApplicableForTitleBarIcons)
{
this.DrawTitleBarButtons(window, flags, outRect,
showAdditions
? this.TitleBarButtons.Append(additionsButton)
: this.TitleBarButtons);
}
} }
if (wasFocused) if (wasFocused)
@ -670,7 +672,17 @@ public abstract class Window
Task.FromResult<IDalamudTextureWrap>(tex)); Task.FromResult<IDalamudTextureWrap>(tex));
} }
this.PostDraw(); if (!this.hasError)
{
this.PostDraw();
}
else
{
if (isErrorStylePushed)
{
Style.StyleModelV1.DalamudStandard.Pop();
}
}
this.PostHandlePreset(persistence); this.PostHandlePreset(persistence);
@ -766,8 +778,11 @@ public abstract class Window
} }
} }
private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable<TitleBarButton> buttons) private unsafe void DrawTitleBarButtons()
{ {
var window = ImGuiP.GetCurrentWindow();
var flags = window.Flags;
var titleBarRect = window.TitleBarRect();
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false); ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
@ -802,26 +817,22 @@ public abstract class Window
var max = pos + new Vector2(fontSize, fontSize); var max = pos + new Vector2(fontSize, fontSize);
ImRect bb = new(pos, max); ImRect bb = new(pos, max);
var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0); var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0);
bool hovered, held; bool hovered, held, pressed;
var pressed = false;
if (this.internalIsClickthrough) if (this.internalIsClickthrough)
{ {
hovered = false;
held = false;
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves // ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
if (ImGui.IsMouseHoveringRect(pos, max)) var pad = ImGui.GetStyle().TouchExtraPadding;
{ var rect = new ImRect(pos - pad, max + pad);
hovered = true; hovered = rect.Contains(ImGui.GetMousePos());
// We can't use ImGui native functions here, because they don't work with clickthrough // Temporarily enable inputs
if ((Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0) // This will be reset on next frame, and then enabled again if it is still being hovered
{ if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
held = true; ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
pressed = true;
} // We can't use ImGui native functions here, because they don't work with clickthrough
} pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0;
} }
else else
{ {
@ -850,7 +861,7 @@ public abstract class Window
return pressed; return pressed;
} }
foreach (var button in buttons.OrderBy(x => x.Priority)) foreach (var button in this.allButtons)
{ {
if (this.internalIsClickthrough && !button.AvailableClickthrough) if (this.internalIsClickthrough && !button.AvailableClickthrough)
return; return;
@ -897,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);
@ -1004,7 +1015,7 @@ public abstract class Window
/// <summary> /// <summary>
/// Gets or sets an action that is called when the button is clicked. /// Gets or sets an action that is called when the button is clicked.
/// </summary> /// </summary>
public Action<ImGuiMouseButton> Click { get; set; } public Action<ImGuiMouseButton>? Click { get; set; }
/// <summary> /// <summary>
/// Gets or sets the priority the button shall be shown in. /// Gets or sets the priority the button shall be shown in.

View file

@ -1,4 +1,5 @@
{ {
"$schema": "https://aka.ms/CsWin32.schema.json", "$schema": "https://aka.ms/CsWin32.schema.json",
"useSafeHandles": false,
"allowMarshaling": false "allowMarshaling": false
} }

View file

@ -79,7 +79,7 @@ public interface IPlayerState : IDalamudService
bool IsLevelSynced { get; } bool IsLevelSynced { get; }
/// <summary> /// <summary>
/// Gets the effective level of the local character. /// Gets the effective level of the local character, taking level sync into account.
/// </summary> /// </summary>
short EffectiveLevel { get; } short EffectiveLevel { get; }

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures; using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
@ -186,6 +187,17 @@ public interface ITextureProvider : IDalamudService
string? debugName = null, string? debugName = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary>Creates a texture by drawing a SeString onto it.</summary>
/// <param name="text">SeString to render.</param>
/// <param name="drawParams">Parameters for drawing.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <returns>The new texture.</returns>
/// <remarks>Can be only be used from the main thread.</remarks>
public IDalamudTextureWrap CreateTextureFromSeString(
ReadOnlySpan<byte> text,
scoped in SeStringDrawParams drawParams = default,
string? debugName = null);
/// <summary>Gets the supported bitmap decoders.</summary> /// <summary>Gets the supported bitmap decoders.</summary>
/// <returns>The supported bitmap decoders.</returns> /// <returns>The supported bitmap decoders.</returns>
/// <remarks> /// <remarks>

View file

@ -1,6 +1,8 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Windows.Win32.Foundation;
namespace Dalamud; namespace Dalamud;
/// <summary> /// <summary>
@ -12,11 +14,11 @@ namespace Dalamud;
/// </remarks> /// </remarks>
public static class SafeMemory public static class SafeMemory
{ {
private static readonly SafeHandle Handle; private static readonly HANDLE Handle;
static SafeMemory() static SafeMemory()
{ {
Handle = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle(); Handle = Windows.Win32.PInvoke.GetCurrentProcess();
} }
/// <summary> /// <summary>
@ -28,6 +30,12 @@ public static class SafeMemory
/// <returns>Whether the read succeeded.</returns> /// <returns>Whether the read succeeded.</returns>
public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer) public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer)
{ {
if (Handle.IsNull)
{
buffer = [];
return false;
}
buffer = new byte[count <= 0 ? 0 : count]; buffer = new byte[count <= 0 ? 0 : count];
fixed (byte* p = buffer) fixed (byte* p = buffer)
{ {
@ -54,6 +62,9 @@ public static class SafeMemory
/// <returns>Whether the write succeeded.</returns> /// <returns>Whether the write succeeded.</returns>
public static unsafe bool WriteBytes(IntPtr address, byte[] buffer) public static unsafe bool WriteBytes(IntPtr address, byte[] buffer)
{ {
if (Handle.IsNull)
return false;
if (buffer.Length == 0) if (buffer.Length == 0)
return true; return true;

View file

@ -1,4 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing; using System.Drawing;
@ -294,18 +294,18 @@ internal sealed class LoadingDialog
? null ? null
: Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe")); : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
fixed (void* pszEmpty = "-") fixed (char* pszEmpty = "-")
fixed (void* pszWindowTitle = "Dalamud") fixed (char* pszWindowTitle = "Dalamud")
fixed (void* pszDalamudBoot = "Dalamud.Boot.dll") fixed (char* pszDalamudBoot = "Dalamud.Boot.dll")
fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") fixed (char* pszThemesManifestResourceName = "RT_MANIFEST_THEMES")
fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) fixed (char* pszHide = Loc.Localize("LoadingDialogHide", "Hide"))
fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs")) fixed (char* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs"))
fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs")) fixed (char* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs"))
{ {
var taskDialogButton = new TASKDIALOG_BUTTON var taskDialogButton = new TASKDIALOG_BUTTON
{ {
nButtonID = IDOK, nButtonID = IDOK,
pszButtonText = (ushort*)pszHide, pszButtonText = pszHide,
}; };
var taskDialogConfig = new TASKDIALOGCONFIG var taskDialogConfig = new TASKDIALOGCONFIG
{ {
@ -318,8 +318,8 @@ internal sealed class LoadingDialog
(int)TDF_CALLBACK_TIMER | (int)TDF_CALLBACK_TIMER |
(extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN), (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN),
dwCommonButtons = 0, dwCommonButtons = 0,
pszWindowTitle = (ushort*)pszWindowTitle, pszWindowTitle = pszWindowTitle,
pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle, pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (char*)extractedIcon.Handle,
pszMainInstruction = null, pszMainInstruction = null,
pszContent = null, pszContent = null,
cButtons = 1, cButtons = 1,
@ -329,9 +329,9 @@ internal sealed class LoadingDialog
pRadioButtons = null, pRadioButtons = null,
nDefaultRadioButton = 0, nDefaultRadioButton = 0,
pszVerificationText = null, pszVerificationText = null,
pszExpandedInformation = (ushort*)pszEmpty, pszExpandedInformation = pszEmpty,
pszExpandedControlText = (ushort*)pszShowLatestLogs, pszExpandedControlText = pszShowLatestLogs,
pszCollapsedControlText = (ushort*)pszHideLatestLogs, pszCollapsedControlText = pszHideLatestLogs,
pszFooterIcon = null, pszFooterIcon = null,
pszFooter = null, pszFooter = null,
pfCallback = &HResultFuncBinder, pfCallback = &HResultFuncBinder,
@ -348,8 +348,8 @@ internal sealed class LoadingDialog
{ {
cbSize = (uint)sizeof(ACTCTXW), cbSize = (uint)sizeof(ACTCTXW),
dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID, dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID,
lpResourceName = (ushort*)pszThemesManifestResourceName, lpResourceName = pszThemesManifestResourceName,
hModule = GetModuleHandleW((ushort*)pszDalamudBoot), hModule = GetModuleHandleW(pszDalamudBoot),
}; };
hActCtx = CreateActCtxW(&actctx); hActCtx = CreateActCtxW(&actctx);
if (hActCtx == default) if (hActCtx == default)

View file

@ -11,12 +11,12 @@ public enum DalamudAssetPurpose
Empty = 0, Empty = 0,
/// <summary> /// <summary>
/// The asset is a .png file, and can be purposed as a <see cref="SharpDX.Direct3D11.Texture2D"/>. /// The asset is a .png file, and can be purposed as a <see cref="TerraFX.Interop.DirectX.ID3D11Texture2D"/>.
/// </summary> /// </summary>
TextureFromPng = 10, TextureFromPng = 10,
/// <summary> /// <summary>
/// The asset is a raw texture, and can be purposed as a <see cref="SharpDX.Direct3D11.Texture2D"/>. /// The asset is a raw texture, and can be purposed as a <see cref="TerraFX.Interop.DirectX.ID3D11Texture2D"/>.
/// </summary> /// </summary>
TextureFromRaw = 1001, TextureFromRaw = 1001,

View file

@ -30,8 +30,8 @@ internal static class ClipboardFormats
private static unsafe uint ClipboardFormatFromName(ReadOnlySpan<char> name) private static unsafe uint ClipboardFormatFromName(ReadOnlySpan<char> name)
{ {
uint cf; uint cf;
fixed (void* p = name) fixed (char* p = name)
cf = RegisterClipboardFormatW((ushort*)p); cf = RegisterClipboardFormatW(p);
if (cf != 0) if (cf != 0)
return cf; return cf;
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ??

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

@ -1,7 +1,8 @@
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.FileSystem; using Windows.Win32.Storage.FileSystem;
namespace Dalamud.Utility; namespace Dalamud.Utility;
@ -47,30 +48,39 @@ public static class FilesystemUtil
// Open the temp file // Open the temp file
var tempPath = path + ".tmp"; var tempPath = path + ".tmp";
using var tempFile = Windows.Win32.PInvoke.CreateFile( var tempFile = Windows.Win32.PInvoke.CreateFile(
tempPath, tempPath,
(uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE),
FILE_SHARE_MODE.FILE_SHARE_NONE, FILE_SHARE_MODE.FILE_SHARE_NONE,
null, null,
FILE_CREATION_DISPOSITION.CREATE_ALWAYS, FILE_CREATION_DISPOSITION.CREATE_ALWAYS,
FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL,
null); HANDLE.Null);
if (tempFile.IsInvalid) if (tempFile.IsNull)
throw new Win32Exception(); throw new Win32Exception();
// Write the data // Write the data
uint bytesWritten = 0; uint bytesWritten = 0;
if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan<byte>(bytes), &bytesWritten, null)) fixed (byte* ptr = bytes)
throw new Win32Exception(); {
if (!Windows.Win32.PInvoke.WriteFile(tempFile, ptr, (uint)bytes.Length, &bytesWritten, null))
throw new Win32Exception();
}
if (bytesWritten != bytes.Length) if (bytesWritten != bytes.Length)
{
Windows.Win32.PInvoke.CloseHandle(tempFile);
throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})");
}
if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile))
{
Windows.Win32.PInvoke.CloseHandle(tempFile);
throw new Win32Exception(); throw new Win32Exception();
}
tempFile.Close(); Windows.Win32.PInvoke.CloseHandle(tempFile);
if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH))
throw new Win32Exception(); throw new Win32Exception();

View file

@ -57,60 +57,60 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
static ManagedIStream? ToManagedObject(void* pThis) => static ManagedIStream? ToManagedObject(void* pThis) =>
GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream; GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) =>
ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0);
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0);
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) =>
ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) =>
ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int SeekStatic( static int SeekStatic(
IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) =>
ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) =>
ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int CopyToStatic( static int CopyToStatic(
IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead,
ULARGE_INTEGER* pcbWritten) => ULARGE_INTEGER* pcbWritten) =>
ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int CommitStatic(IStream* pThis, uint grfCommitFlags) => static int CommitStatic(IStream* pThis, uint grfCommitFlags) =>
ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED; static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int UnlockRegionStatic( static int UnlockRegionStatic(
IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) =>
ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED; ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED; static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED;
} }

View file

@ -88,7 +88,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
fixed (char* pPath = path) fixed (char* pPath = path)
{ {
SHCreateStreamOnFileEx( SHCreateStreamOnFileEx(
(ushort*)pPath, pPath,
grfMode, grfMode,
(uint)attributes, (uint)attributes,
fCreate, fCreate,
@ -115,7 +115,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
{ {
fixed (char* pName = name) fixed (char* pName = name)
{ {
var option = new PROPBAG2 { pstrName = (ushort*)pName }; var option = new PROPBAG2 { pstrName = pName };
return obj.Write(1, &option, &varValue); return obj.Write(1, &option, &varValue);
} }
} }
@ -145,7 +145,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
try try
{ {
fixed (char* pName = name) fixed (char* pName = name)
return obj.SetMetadataByName((ushort*)pName, &propVarValue); return obj.SetMetadataByName(pName, &propVarValue);
} }
finally finally
{ {
@ -165,7 +165,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name)
{ {
fixed (char* pName = name) fixed (char* pName = name)
return obj.RemoveMetadataByName((ushort*)pName); return obj.RemoveMetadataByName(pName);
} }
[LibraryImport("propsys.dll")] [LibraryImport("propsys.dll")]

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>
@ -858,7 +858,7 @@ public static partial class Util
var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2); var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2);
var dropFilesSize = sizeof(DROPFILES); var dropFilesSize = sizeof(DROPFILES);
var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle( var hGlobal = Win32_PInvoke.GlobalAlloc(
GLOBAL_ALLOC_FLAGS.GHND, GLOBAL_ALLOC_FLAGS.GHND,
// struct size + size of encoded strings + null terminator for each // struct size + size of encoded strings + null terminator for each
// string + two null terminators for end of list // string + two null terminators for end of list
@ -896,12 +896,11 @@ public static partial class Util
{ {
Win32_PInvoke.SetClipboardData( Win32_PInvoke.SetClipboardData(
(uint)CLIPBOARD_FORMAT.CF_HDROP, (uint)CLIPBOARD_FORMAT.CF_HDROP,
hGlobal); (Windows.Win32.Foundation.HANDLE)hGlobal.Value);
Win32_PInvoke.CloseClipboard(); Win32_PInvoke.CloseClipboard();
return true; return true;
} }
hGlobal.Dispose();
return false; return false;
} }

View file

@ -1,51 +0,0 @@
using System.Numerics;
namespace Dalamud.Utility;
/// <summary>
/// Extension methods for System.Numerics.VectorN and SharpDX.VectorN.
/// </summary>
public static class VectorExtensions
{
/// <summary>
/// Converts a SharpDX vector to System.Numerics.
/// </summary>
/// <param name="vec">Vector to convert.</param>
/// <returns>A converted vector.</returns>
public static Vector2 ToSystem(this SharpDX.Vector2 vec) => new(x: vec.X, y: vec.Y);
/// <summary>
/// Converts a SharpDX vector to System.Numerics.
/// </summary>
/// <param name="vec">Vector to convert.</param>
/// <returns>A converted vector.</returns>
public static Vector3 ToSystem(this SharpDX.Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z);
/// <summary>
/// Converts a SharpDX vector to System.Numerics.
/// </summary>
/// <param name="vec">Vector to convert.</param>
/// <returns>A converted vector.</returns>
public static Vector4 ToSystem(this SharpDX.Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W);
/// <summary>
/// Converts a System.Numerics vector to SharpDX.
/// </summary>
/// <param name="vec">Vector to convert.</param>
/// <returns>A converted vector.</returns>
public static SharpDX.Vector2 ToSharpDX(this Vector2 vec) => new(x: vec.X, y: vec.Y);
/// <summary>
/// Converts a System.Numerics vector to SharpDX.
/// </summary>
/// <param name="vec">Vector to convert.</param>
/// <returns>A converted vector.</returns>
public static SharpDX.Vector3 ToSharpDX(this Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z);
/// <summary>
/// Converts a System.Numerics vector to SharpDX.
/// </summary>
/// <param name="vec">Vector to convert.</param>
/// <returns>A converted vector.</returns>
public static SharpDX.Vector4 ToSharpDX(this Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W);
}

View file

@ -119,7 +119,7 @@ std::wstring describe_module(const std::filesystem::path& path) {
return std::format(L"<error: GetFileVersionInfoSizeW#2 returned {}>", GetLastError()); return std::format(L"<error: GetFileVersionInfoSizeW#2 returned {}>", GetLastError());
UINT size = 0; UINT size = 0;
std::wstring version = L"v?.?.?.?"; std::wstring version = L"v?.?.?.?";
if (LPVOID lpBuffer; VerQueryValueW(block.data(), L"\\", &lpBuffer, &size)) { if (LPVOID lpBuffer; VerQueryValueW(block.data(), L"\\", &lpBuffer, &size)) {
const auto& v = *static_cast<const VS_FIXEDFILEINFO*>(lpBuffer); const auto& v = *static_cast<const VS_FIXEDFILEINFO*>(lpBuffer);
@ -176,7 +176,7 @@ const std::map<HMODULE, size_t>& get_remote_modules() {
std::vector<HMODULE> buf(8192); std::vector<HMODULE> buf(8192);
for (size_t i = 0; i < 64; i++) { for (size_t i = 0; i < 64; i++) {
if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast<DWORD>(std::span(buf).size_bytes()), &needed)) { if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast<DWORD>(std::span(buf).size_bytes()), &needed)) {
std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl; std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl;
break; break;
} else if (needed > std::span(buf).size_bytes()) { } else if (needed > std::span(buf).size_bytes()) {
buf.resize(needed / sizeof(HMODULE) + 16); buf.resize(needed / sizeof(HMODULE) + 16);
@ -201,7 +201,7 @@ const std::map<HMODULE, size_t>& get_remote_modules() {
data[hModule] = nth64.OptionalHeader.SizeOfImage; data[hModule] = nth64.OptionalHeader.SizeOfImage;
} }
return data; return data;
}(); }();
@ -292,35 +292,43 @@ std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef =
void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) { void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) {
std::vector<EXCEPTION_RECORD> exRecs; std::vector<EXCEPTION_RECORD> exRecs;
if (ex.ExceptionRecord) { if (ex.ExceptionRecord)
{
size_t rec_index = 0; size_t rec_index = 0;
size_t read; size_t read;
exRecs.emplace_back();
for (auto pRemoteExRec = ex.ExceptionRecord; for (auto pRemoteExRec = ex.ExceptionRecord;
pRemoteExRec pRemoteExRec && rec_index < 64;
&& rec_index < 64 rec_index++)
&& ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read) {
&& read >= offsetof(EXCEPTION_RECORD, ExceptionInformation) exRecs.emplace_back();
&& read >= static_cast<size_t>(reinterpret_cast<const char*>(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast<const char*>(&exRecs.back()));
rec_index++) { if (!ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
|| read < offsetof(EXCEPTION_RECORD, ExceptionInformation)
|| read < static_cast<size_t>(reinterpret_cast<const char*>(&exRecs.back().ExceptionInformation[exRecs.
back().NumberParameters]) - reinterpret_cast<const char*>(&exRecs.back())))
{
exRecs.pop_back();
break;
}
log << std::format(L"\nException Info #{}\n", rec_index); log << std::format(L"\nException Info #{}\n", rec_index);
log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode); log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode);
log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags); log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags);
log << std::format(L"Address: {:X}\n", reinterpret_cast<size_t>(exRecs.back().ExceptionAddress)); log << std::format(L"Address: {:X}\n", reinterpret_cast<size_t>(exRecs.back().ExceptionAddress));
if (!exRecs.back().NumberParameters) if (exRecs.back().NumberParameters)
continue; {
log << L"Parameters: "; log << L"Parameters: ";
for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) { for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i)
if (i != 0) {
log << L", "; if (i != 0)
log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]); log << L", ";
log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]);
}
} }
pRemoteExRec = exRecs.back().ExceptionRecord; pRemoteExRec = exRecs.back().ExceptionRecord;
exRecs.emplace_back();
} }
exRecs.pop_back();
} }
log << L"\nCall Stack\n{"; log << L"\nCall Stack\n{";
@ -410,7 +418,7 @@ void print_exception_info_extended(const EXCEPTION_POINTERS& ex, const CONTEXT&
std::wstring escape_shell_arg(const std::wstring& arg) { std::wstring escape_shell_arg(const std::wstring& arg) {
// https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way // https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
std::wstring res; std::wstring res;
if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) { if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) {
res.append(arg); res.append(arg);
@ -504,7 +512,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
filePath.emplace(pFilePath); filePath.emplace(pFilePath);
std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
mz_zip_archive zipa{}; mz_zip_archive zipa{};
zipa.m_pIO_opaque = &fileStream; zipa.m_pIO_opaque = &fileStream;
zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t { zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t {
@ -566,7 +574,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hLogFile == INVALID_HANDLE_VALUE) if (hLogFile == INVALID_HANDLE_VALUE)
throw_last_error(std::format("indiv. log file: CreateFileW({})", ws_to_u8(logFilePath.wstring()))); throw_last_error(std::format("indiv. log file: CreateFileW({})", ws_to_u8(logFilePath.wstring())));
std::unique_ptr<void, decltype(&CloseHandle)> hLogFileClose(hLogFile, &CloseHandle); std::unique_ptr<void, decltype(&CloseHandle)> hLogFileClose(hLogFile, &CloseHandle);
LARGE_INTEGER size, baseOffset{}; LARGE_INTEGER size, baseOffset{};
@ -695,7 +703,7 @@ int main() {
// IFileSaveDialog only works on STA // IFileSaveDialog only works on STA
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
std::vector<std::wstring> args; std::vector<std::wstring> args;
if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) {
for (auto i = 0; i < argc; i++) for (auto i = 0; i < argc; i++)
@ -823,14 +831,14 @@ int main() {
hr = pOleWindow->GetWindow(&hwndProgressDialog); hr = pOleWindow->GetWindow(&hwndProgressDialog);
if (SUCCEEDED(hr)) if (SUCCEEDED(hr))
{ {
SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SetForegroundWindow(hwndProgressDialog); SetForegroundWindow(hwndProgressDialog);
} }
pOleWindow->Release(); pOleWindow->Release();
} }
} }
else { else {
std::cerr << "Failed to create progress window" << std::endl; std::cerr << "Failed to create progress window" << std::endl;
@ -852,14 +860,14 @@ int main() {
https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/utils/DbgHelpDyn.cpp https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/utils/DbgHelpDyn.cpp
*/ */
if (g_bSymbolsAvailable) { if (g_bSymbolsAvailable) {
SymRefreshModuleList(g_hProcess); SymRefreshModuleList(g_hProcess);
} }
else if(!assetDir.empty()) else if(!assetDir.empty())
{ {
auto symbol_search_path = std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring()); auto symbol_search_path = std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring());
g_bSymbolsAvailable = SymInitializeW(g_hProcess, symbol_search_path.c_str(), true); g_bSymbolsAvailable = SymInitializeW(g_hProcess, symbol_search_path.c_str(), true);
std::wcout << std::format(L"Init symbols with PDB at {}", symbol_search_path) << std::endl; std::wcout << std::format(L"Init symbols with PDB at {}", symbol_search_path) << std::endl;
@ -870,12 +878,12 @@ int main() {
g_bSymbolsAvailable = SymInitializeW(g_hProcess, nullptr, true); g_bSymbolsAvailable = SymInitializeW(g_hProcess, nullptr, true);
std::cout << "Init symbols without PDB" << std::endl; std::cout << "Init symbols without PDB" << std::endl;
} }
if (!g_bSymbolsAvailable) { if (!g_bSymbolsAvailable) {
std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl; std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl;
} }
if (pProgressDialog) if (pProgressDialog)
pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL); pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL);
std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0'); std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0');
@ -930,13 +938,23 @@ int main() {
} while (false); } while (false);
} }
const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT;
std::wostringstream log; std::wostringstream log;
log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl; if (!is_external_event)
{
log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl;
}
else
{
log << L"CLR error occurred" << std::endl;
}
if (shutup) if (shutup)
log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl; log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl;
if (dumpPath.empty()) if (dumpPath.empty())
log << L"Dump skipped" << std::endl; log << L"Dump skipped" << std::endl;
else if (dumpError.empty()) else if (dumpError.empty())
@ -949,9 +967,19 @@ int main() {
if (pProgressDialog) if (pProgressDialog)
pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL); pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL);
std::wstring window_log_str;
// Cut the log here for external events, the rest is unreadable and doesn't matter since we can't get
// symbols for mixed-mode stacks yet.
if (is_external_event)
window_log_str = log.str();
SymRefreshModuleList(GetCurrentProcess()); SymRefreshModuleList(GetCurrentProcess());
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log); print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
const auto window_log_str = log.str();
if (!is_external_event)
window_log_str = log.str();
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log); print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
std::wofstream(logPath) << log.str(); std::wofstream(logPath) << log.str();
@ -986,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;
@ -1003,7 +1031,7 @@ int main() {
R"aa(<a href="help">Help</a> | <a href="logdir">Open log directory</a> | <a href="logfile">Open log file</a>)aa" R"aa(<a href="help">Help</a> | <a href="logdir">Open log directory</a> | <a href="logfile">Open log file</a>)aa"
); );
#endif #endif
// Can't do this, xiv stops pumping messages here // Can't do this, xiv stops pumping messages here
//config.hwndParent = FindWindowA("FFXIVGAME", NULL); //config.hwndParent = FindWindowA("FFXIVGAME", NULL);
@ -1056,13 +1084,13 @@ int main() {
return (*reinterpret_cast<decltype(callback)*>(dwRefData))(hwnd, uNotification, wParam, lParam); return (*reinterpret_cast<decltype(callback)*>(dwRefData))(hwnd, uNotification, wParam, lParam);
}; };
config.lpCallbackData = reinterpret_cast<LONG_PTR>(&callback); config.lpCallbackData = reinterpret_cast<LONG_PTR>(&callback);
if (pProgressDialog) { if (pProgressDialog) {
pProgressDialog->StopProgressDialog(); pProgressDialog->StopProgressDialog();
pProgressDialog->Release(); pProgressDialog->Release();
pProgressDialog = NULL; pProgressDialog = NULL;
} }
const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); }; const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); };
if (shutup) { if (shutup) {

View file

@ -26,10 +26,8 @@
<PackageVersion Include="sqlite-net-pcl" Version="1.8.116" /> <PackageVersion Include="sqlite-net-pcl" Version="1.8.116" />
<!-- DirectX / Win32 --> <!-- DirectX / Win32 -->
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.2" /> <PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.5" />
<PackageVersion Include="SharpDX.Direct3D11" Version="4.2.0" /> <PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.259" />
<PackageVersion Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
<!-- Logging --> <!-- Logging -->
<PackageVersion Include="Serilog" Version="4.0.2" /> <PackageVersion Include="Serilog" Version="4.0.2" />

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using Nuke.Common; using Nuke.Common;
using Nuke.Common.Execution; using Nuke.Common.Execution;
using Nuke.Common.Git; using Nuke.Common.Git;
@ -128,7 +127,7 @@ public class DalamudBuild : NukeBuild
if (IsCIBuild) if (IsCIBuild)
{ {
s = s s = s
.SetProcessArgumentConfigurator(a => a.Add("/clp:NoSummary")); // Disable MSBuild summary on CI builds .SetProcessAdditionalArguments("/clp:NoSummary"); // Disable MSBuild summary on CI builds
} }
// We need to emit compiler generated files for the docs build, since docfx can't run generators directly // We need to emit compiler generated files for the docs build, since docfx can't run generators directly
// TODO: This fails every build after this because of redefinitions... // TODO: This fails every build after this because of redefinitions...
@ -238,7 +237,6 @@ public class DalamudBuild : NukeBuild
.SetProject(InjectorProjectFile) .SetProject(InjectorProjectFile)
.SetConfiguration(Configuration)); .SetConfiguration(Configuration));
FileSystemTasks.DeleteDirectory(ArtifactsDirectory); ArtifactsDirectory.CreateOrCleanDirectory();
Directory.CreateDirectory(ArtifactsDirectory);
}); });
} }

View file

@ -11,7 +11,7 @@
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Nuke.Common" Version="6.2.1" /> <PackageReference Include="Nuke.Common" Version="10.1.0" />
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="10.0.0" /> <PackageReference Include="System.Runtime.Serialization.Formatters" Version="10.0.0" />
<PackageReference Remove="Microsoft.CodeAnalysis.BannedApiAnalyzers" /> <PackageReference Remove="Microsoft.CodeAnalysis.BannedApiAnalyzers" />
</ItemGroup> </ItemGroup>

View file

@ -1,7 +1,12 @@
using System.Runtime.CompilerServices; using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dalamud.Bindings.ImGui; namespace Dalamud.Bindings.ImGui;
/// <summary>
/// A structure representing a dynamic array for unmanaged types.
/// </summary>
public unsafe struct ImVector public unsafe struct ImVector
{ {
public readonly int Size; public readonly int Size;
@ -15,23 +20,23 @@ public unsafe struct ImVector
Data = data; Data = data;
} }
public ref T Ref<T>(int index) public readonly ref T Ref<T>(int index) => ref Unsafe.AsRef<T>((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
{
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
}
public IntPtr Address<T>(int index) public readonly nint Address<T>(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
{
return (IntPtr)((byte*)Data + index * Unsafe.SizeOf<T>());
}
} }
/// <summary> /// <summary>
/// A structure representing a dynamic array for unmanaged types. /// A structure representing a dynamic array for unmanaged types.
/// </summary> /// </summary>
/// <typeparam name="T">The type of elements in the vector, must be unmanaged.</typeparam> /// <typeparam name="T">The type of elements in the vector, must be unmanaged.</typeparam>
public unsafe struct ImVector<T> where T : unmanaged [StructLayout(LayoutKind.Sequential)]
public unsafe struct ImVector<T> : IEnumerable<T>
where T : unmanaged
{ {
private int size;
private int capacity;
private T* data;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer. /// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer.
/// </summary> /// </summary>
@ -45,11 +50,6 @@ public unsafe struct ImVector<T> where T : unmanaged
this.data = data; this.data = data;
} }
private int size;
private int capacity;
private unsafe T* data;
/// <summary> /// <summary>
/// Gets or sets the element at the specified index. /// Gets or sets the element at the specified index.
/// </summary> /// </summary>
@ -58,80 +58,72 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <exception cref="IndexOutOfRangeException">Thrown when the index is out of range.</exception> /// <exception cref="IndexOutOfRangeException">Thrown when the index is out of range.</exception>
public T this[int index] public T this[int index]
{ {
get readonly get
{ {
if (index < 0 || index >= size) if (index < 0 || index >= this.size)
{
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} return this.data[index];
return data[index];
} }
set set
{ {
if (index < 0 || index >= size) if (index < 0 || index >= this.size)
{
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} this.data[index] = value;
data[index] = value;
} }
} }
/// <summary> /// <summary>
/// Gets a pointer to the first element of the vector. /// Gets a pointer to the first element of the vector.
/// </summary> /// </summary>
public readonly T* Data => data; public readonly T* Data => this.data;
/// <summary> /// <summary>
/// Gets a pointer to the first element of the vector. /// Gets a pointer to the first element of the vector.
/// </summary> /// </summary>
public readonly T* Front => data; public readonly T* Front => this.data;
/// <summary> /// <summary>
/// Gets a pointer to the last element of the vector. /// Gets a pointer to the last element of the vector.
/// </summary> /// </summary>
public readonly T* Back => size > 0 ? data + size - 1 : null; public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null;
/// <summary> /// <summary>
/// Gets or sets the capacity of the vector. /// Gets or sets the capacity of the vector.
/// </summary> /// </summary>
public int Capacity public int Capacity
{ {
readonly get => capacity; readonly get => this.capacity;
set set
{ {
if (capacity == value) ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity));
{ if (this.capacity == value)
return; return;
}
if (data == null) if (this.data == null)
{ {
data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); this.data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
} }
else else
{ {
int newSize = Math.Min(size, value); var newSize = Math.Min(this.size, value);
T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T))); Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
ImGui.MemFree(data); ImGui.MemFree(this.data);
data = newData; this.data = newData;
size = newSize; this.size = newSize;
} }
capacity = value; this.capacity = value;
// Clear the rest of the data // Clear the rest of the data
for (int i = size; i < capacity; i++) new Span<T>(this.data + this.size, this.capacity - this.size).Clear();
{
data[i] = default;
}
} }
} }
/// <summary> /// <summary>
/// Gets the number of elements in the vector. /// Gets the number of elements in the vector.
/// </summary> /// </summary>
public readonly int Size => size; public readonly int Size => this.size;
/// <summary> /// <summary>
/// Grows the capacity of the vector to at least the specified value. /// Grows the capacity of the vector to at least the specified value.
@ -139,10 +131,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="newCapacity">The new capacity.</param> /// <param name="newCapacity">The new capacity.</param>
public void Grow(int newCapacity) public void Grow(int newCapacity)
{ {
if (newCapacity > capacity) var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8;
{ this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity;
Capacity = newCapacity * 2;
}
} }
/// <summary> /// <summary>
@ -151,10 +141,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="size">The minimum capacity required.</param> /// <param name="size">The minimum capacity required.</param>
public void EnsureCapacity(int size) public void EnsureCapacity(int size)
{ {
if (size > capacity) if (size > this.capacity)
{
Grow(size); Grow(size);
}
} }
/// <summary> /// <summary>
@ -164,25 +152,46 @@ public unsafe struct ImVector<T> where T : unmanaged
public void Resize(int newSize) public void Resize(int newSize)
{ {
EnsureCapacity(newSize); EnsureCapacity(newSize);
size = newSize; this.size = newSize;
} }
/// <summary> /// <summary>
/// Clears all elements from the vector. /// Clears all elements from the vector.
/// </summary> /// </summary>
public void Clear() public void Clear() => this.size = 0;
/// <summary>
/// Adds an element to the end of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
[OverloadResolutionPriority(1)]
public void PushBack(T value)
{ {
size = 0; this.EnsureCapacity(this.size + 1);
this.data[this.size++] = value;
} }
/// <summary> /// <summary>
/// Adds an element to the end of the vector. /// Adds an element to the end of the vector.
/// </summary> /// </summary>
/// <param name="value">The value to add.</param> /// <param name="value">The value to add.</param>
public void PushBack(T value) [OverloadResolutionPriority(2)]
public void PushBack(in T value)
{ {
EnsureCapacity(size + 1); EnsureCapacity(this.size + 1);
data[size++] = value; this.data[this.size++] = value;
}
/// <summary>
/// Adds an element to the front of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
public void PushFront(in T value)
{
if (this.size == 0)
this.PushBack(value);
else
this.Insert(0, value);
} }
/// <summary> /// <summary>
@ -190,48 +199,126 @@ public unsafe struct ImVector<T> where T : unmanaged
/// </summary> /// </summary>
public void PopBack() public void PopBack()
{ {
if (size > 0) if (this.size > 0)
{ {
size--; this.size--;
} }
} }
public ref T Insert(int index, in T v) {
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
this.EnsureCapacity(this.size + 1);
if (index < this.size)
{
Buffer.MemoryCopy(
this.data + index,
this.data + index + 1,
(this.size - index) * sizeof(T),
(this.size - index) * sizeof(T));
}
this.data[index] = v;
this.size++;
return ref this.data[index];
}
public Span<T> InsertRange(int index, ReadOnlySpan<T> v)
{
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
this.EnsureCapacity(this.size + v.Length);
if (index < this.size)
{
Buffer.MemoryCopy(
this.data + index,
this.data + index + v.Length,
(this.size - index) * sizeof(T),
(this.size - index) * sizeof(T));
}
var dstSpan = new Span<T>(this.data + index, v.Length);
v.CopyTo(new(this.data + index, v.Length));
this.size += v.Length;
return dstSpan;
}
/// <summary> /// <summary>
/// Frees the memory allocated for the vector. /// Frees the memory allocated for the vector.
/// </summary> /// </summary>
public void Free() public void Free()
{ {
if (data != null) if (this.data != null)
{ {
ImGui.MemFree(data); ImGui.MemFree(this.data);
data = null; this.data = null;
size = 0; this.size = 0;
capacity = 0; this.capacity = 0;
} }
} }
public ref T Ref(int index) public readonly ref T Ref(int index)
{ {
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>()); return ref Unsafe.AsRef<T>((byte*)Data + (index * Unsafe.SizeOf<T>()));
} }
public ref TCast Ref<TCast>(int index) public readonly ref TCast Ref<TCast>(int index)
{ {
return ref Unsafe.AsRef<TCast>((byte*)Data + index * Unsafe.SizeOf<TCast>()); return ref Unsafe.AsRef<TCast>((byte*)Data + (index * Unsafe.SizeOf<TCast>()));
} }
public void* Address(int index) public readonly void* Address(int index)
{ {
return (byte*)Data + index * Unsafe.SizeOf<T>(); return (byte*)Data + (index * Unsafe.SizeOf<T>());
} }
public void* Address<TCast>(int index) public readonly void* Address<TCast>(int index)
{ {
return (byte*)Data + index * Unsafe.SizeOf<TCast>(); return (byte*)Data + (index * Unsafe.SizeOf<TCast>());
} }
public ImVector* ToUntyped() public readonly ImVector* ToUntyped()
{ {
return (ImVector*)Unsafe.AsPointer(ref this); return (ImVector*)Unsafe.AsPointer(ref Unsafe.AsRef(in this));
}
public readonly Span<T> AsSpan() => new(this.data, this.size);
public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size);
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
public struct Enumerator(T* begin, T* end) : IEnumerator<T>, IEnumerable<T>
{
private T* current = null;
public readonly ref T Current => ref *this.current;
readonly T IEnumerator<T>.Current => this.Current;
readonly object IEnumerator.Current => this.Current;
public bool MoveNext()
{
var next = this.current == null ? begin : this.current + 1;
if (next == end)
return false;
this.current = next;
return true;
}
public void Reset() => this.current = null;
public readonly Enumerator GetEnumerator() => new(begin, end);
readonly void IDisposable.Dispose()
{
}
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
} }
} }

@ -1 +1 @@
Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77 Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f

@ -1 +1 @@
Subproject commit 27c8565f631b004c3266373890e41ecc627f775b Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c

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>