mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-17 14:14:17 +01:00
Compare commits
212 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f142fb1058 | ||
|
|
46954e6add | ||
|
|
01901c237a | ||
|
|
cdf4e27355 | ||
|
|
a843079a6b | ||
|
|
ddd85513ba | ||
|
|
89fbe6c8b0 | ||
|
|
1c1b60efee | ||
|
|
2e7c48316f | ||
|
|
b0a0fafb53 | ||
|
|
8334836b6a | ||
|
|
8a742e7e59 | ||
|
|
56325afa7f | ||
|
|
1bff6abae9 | ||
|
|
d7935d6dd4 | ||
|
|
a715725a9d | ||
|
|
bc8e986c11 | ||
|
|
ffd99d5791 | ||
|
|
20af5b40c7 | ||
|
|
a1409096fd | ||
|
|
fecba89710 | ||
|
|
b57b96b9a0 | ||
|
|
180676fe47 | ||
|
|
2d096d9b33 | ||
|
|
e100ec2abd | ||
|
|
71b0a757e9 | ||
|
|
0b55dc3e10 | ||
|
|
4d9751ea5f | ||
|
|
a39763f161 | ||
|
|
201c9cfcf2 | ||
|
|
e07bda7e58 | ||
|
|
b88a6bb616 | ||
|
|
e53ccdbcc0 | ||
|
|
97df73acea | ||
|
|
2806e59dba | ||
|
|
24caa1cb18 | ||
|
|
5d08170333 | ||
|
|
d0110f7251 | ||
|
|
2dbae05522 | ||
|
|
8ed1af30df | ||
|
|
e8485dee25 | ||
|
|
0072f49fe8 | ||
|
|
c45c6aafe1 | ||
|
|
2029a0f8a6 | ||
|
|
bcb8094c2d | ||
|
|
624191d1e0 | ||
|
|
c254c8600e | ||
|
|
61376fe84e | ||
|
|
2f5f52b572 | ||
|
|
7199bfb0a9 | ||
|
|
abcddde591 | ||
|
|
2a99108eb1 | ||
|
|
8a5f1fd96d | ||
|
|
652ff59672 | ||
|
|
094483e5a0 | ||
|
|
c50237cf66 | ||
|
|
d4fe523d73 | ||
|
|
9e5723359a | ||
|
|
07f9e03010 | ||
|
|
9cfa81c92d | ||
|
|
b35faf13b5 | ||
|
|
caa869d3ac | ||
|
|
9fd59f736d | ||
|
|
ab5ea34e68 | ||
|
|
501e30e31c | ||
|
|
3d29157391 | ||
|
|
b2d9480f9f | ||
|
|
1ad1343cbc | ||
|
|
61123ce573 | ||
|
|
9f565fafd8 | ||
|
|
88fc933e3f | ||
|
|
e032840ac8 | ||
|
|
1d1db04f04 | ||
|
|
446c7e3877 | ||
|
|
e09c43b8de | ||
|
|
9c2d2b7c1d | ||
|
|
2e5c560ed7 | ||
|
|
45366efd9f | ||
|
|
3c7dbf9f81 | ||
|
|
a36e11574b | ||
|
|
d94cacaac3 | ||
|
|
7cf20fe102 | ||
|
|
98a4c0d4fd | ||
|
|
f85ef995e3 | ||
|
|
e7d4786a1f | ||
|
|
4d949e4a07 | ||
|
|
68ca60fa8c | ||
|
|
411067219e | ||
|
|
fc983458fa | ||
|
|
ddc3113244 | ||
|
|
da7be64fdf | ||
|
|
0112e17fdb | ||
|
|
6f8e33a39c | ||
|
|
ddc743aae1 | ||
|
|
8dcbd52c22 | ||
|
|
1b5fbaa82e | ||
|
|
9bce0d33a6 | ||
|
|
879c210cc6 | ||
|
|
1fe2d54128 | ||
|
|
bfd592abbe | ||
|
|
df0bfc18c3 | ||
|
|
0480693f92 | ||
|
|
3fbc24904a | ||
|
|
5bb212bfaa | ||
|
|
f055af7f7b | ||
|
|
a917ebd856 | ||
|
|
0e6dae9f64 | ||
|
|
4fa4d7f338 | ||
|
|
f198ce46dc | ||
|
|
518b3a4fb3 | ||
|
|
85949072ec | ||
|
|
14e97a1a37 | ||
|
|
f3c826a54b | ||
|
|
fb229a0a12 | ||
|
|
85a7c60dae | ||
|
|
c923884626 | ||
|
|
78781c8988 | ||
|
|
2e24696731 | ||
|
|
b81cb9c74c | ||
|
|
8e8d0246bc | ||
|
|
d47a41b295 | ||
|
|
c9276b1771 | ||
|
|
386828005b | ||
|
|
08c1768286 | ||
|
|
eb9555ee22 | ||
|
|
be3f71dc73 | ||
|
|
e01acb4a80 | ||
|
|
f8725e5f37 | ||
|
|
c3e3e4aa85 | ||
|
|
b82b4f40ce | ||
|
|
4f59e09513 | ||
|
|
0533872a73 | ||
|
|
27a7adfdb9 | ||
|
|
54bac7f32a | ||
|
|
26f119096b | ||
|
|
c51e65e0bd | ||
|
|
874745651b | ||
|
|
ac2d522415 | ||
|
|
ead1c705a4 | ||
|
|
fadf941fa4 | ||
|
|
a31dda7865 | ||
|
|
d7e04ad4ff | ||
|
|
7510c032cc | ||
|
|
d12a9ec7da | ||
|
|
6367a66aad | ||
|
|
edc6962296 | ||
|
|
78ecb721cd | ||
|
|
b8724f7a59 | ||
|
|
d7915c7020 | ||
|
|
170f6e0859 | ||
|
|
325d28ee32 | ||
|
|
29c154f9b5 | ||
|
|
2a60bc61a7 | ||
|
|
166f249e13 | ||
|
|
c525655be6 | ||
|
|
02e0f1d36c | ||
|
|
c661faea6b | ||
|
|
4c3ba35f07 | ||
|
|
d4f1636dd2 | ||
|
|
196a5ef709 | ||
|
|
c136934aa8 | ||
|
|
5e192ef39b | ||
|
|
947518b3d6 | ||
|
|
2cef75bbbe | ||
|
|
ab0500ca6f | ||
|
|
2c1bb76643 | ||
|
|
9a1fae8246 | ||
|
|
8ab7b59ae4 | ||
|
|
7b286c427c | ||
|
|
0d8f577576 | ||
|
|
01d8fc0c7e | ||
|
|
71927a8bf6 | ||
|
|
6a69a6e197 | ||
|
|
cc91916574 | ||
|
|
b7dda599fb | ||
|
|
63b7ecf0d7 | ||
|
|
e4eca842d3 | ||
|
|
c79fa96505 | ||
|
|
ba0cf4c990 | ||
|
|
9a49a9588b | ||
|
|
19a3926051 | ||
|
|
4937a2f4bd | ||
|
|
78ed4a2b01 | ||
|
|
62b9c1f2a1 | ||
|
|
a2e923b051 | ||
|
|
de396e70f8 | ||
|
|
7a8f01f418 | ||
|
|
9d0879148c | ||
|
|
778c82fad2 | ||
|
|
7f2ed9adb6 | ||
|
|
53b94caeb7 | ||
|
|
d1dc81318a | ||
|
|
a48eead85e | ||
|
|
d1bed3ebc5 | ||
|
|
23e7c164d8 | ||
|
|
8a9b47c7a4 | ||
|
|
520e3ea028 | ||
|
|
dd70c5b8ee | ||
|
|
2b2f628096 | ||
|
|
6340afb692 | ||
|
|
928fbba489 | ||
|
|
7bc921f543 | ||
|
|
a37a13e0ba | ||
|
|
e0eff2fe74 | ||
|
|
7d76d27555 | ||
|
|
4e87b4b007 | ||
|
|
8cced4c1d7 | ||
|
|
b18b8b40e5 | ||
|
|
544f8b28bf | ||
|
|
e5451c37af | ||
|
|
40e63f2d9a | ||
|
|
c19ea6ace3 |
196 changed files with 5568 additions and 3569 deletions
161
.github/generate_changelog.py
vendored
161
.github/generate_changelog.py
vendored
|
|
@ -8,7 +8,8 @@ import re
|
|||
import sys
|
||||
import json
|
||||
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:
|
||||
|
|
@ -55,46 +56,132 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]:
|
||||
"""Get commits between two tags. Returns list of (message, author) tuples."""
|
||||
def get_repo_info() -> Tuple[str, str]:
|
||||
"""Get repository owner and name from git remote."""
|
||||
try:
|
||||
remote_url = run_git_command(["config", "--get", "remote.origin.url"])
|
||||
|
||||
# Handle both HTTPS and SSH URLs
|
||||
# SSH: git@github.com:owner/repo.git
|
||||
# HTTPS: https://github.com/owner/repo.git
|
||||
match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url)
|
||||
if match:
|
||||
owner = match.group(1)
|
||||
repo = match.group(2)
|
||||
return owner, repo
|
||||
else:
|
||||
print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except:
|
||||
print("Error: Could not get git remote URL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_commits_between_tags(tag1: str, tag2: str) -> List[str]:
|
||||
"""Get commit SHAs between two tags."""
|
||||
log_output = run_git_command([
|
||||
"log",
|
||||
f"{tag2}..{tag1}",
|
||||
"--format=%s|%an|%h"
|
||||
"--format=%H"
|
||||
])
|
||||
|
||||
commits = []
|
||||
for line in log_output.split("\n"):
|
||||
if "|" in line:
|
||||
message, author, sha = line.split("|", 2)
|
||||
commits.append((message.strip(), author.strip(), sha.strip()))
|
||||
|
||||
commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
|
||||
return commits
|
||||
|
||||
|
||||
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]:
|
||||
"""Filter out commits matching any of the ignore patterns."""
|
||||
def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get PR information for a commit using GitHub API."""
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls"
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
prs = response.json()
|
||||
|
||||
if prs and len(prs) > 0:
|
||||
# Return the first PR (most relevant one)
|
||||
pr = prs[0]
|
||||
return {
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"author": pr["user"]["login"],
|
||||
"url": pr["html_url"]
|
||||
}
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
# Commit might not be associated with a PR
|
||||
return None
|
||||
elif e.response.status_code == 403:
|
||||
print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr)
|
||||
return None
|
||||
else:
|
||||
print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]:
|
||||
"""Get PRs between two tags using GitHub API."""
|
||||
commits = get_commits_between_tags(tag1, tag2)
|
||||
print(f"Found {len(commits)} commits, fetching PR information...")
|
||||
|
||||
prs = []
|
||||
seen_pr_numbers = set()
|
||||
|
||||
for i, commit_sha in enumerate(commits, 1):
|
||||
if i % 10 == 0:
|
||||
print(f"Progress: {i}/{len(commits)} commits processed...")
|
||||
|
||||
pr_info = get_pr_for_commit(commit_sha, owner, repo, token)
|
||||
if pr_info and pr_info["number"] not in seen_pr_numbers:
|
||||
seen_pr_numbers.add(pr_info["number"])
|
||||
prs.append(pr_info)
|
||||
|
||||
return prs
|
||||
|
||||
|
||||
def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]:
|
||||
"""Filter out PRs matching any of the ignore patterns."""
|
||||
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
|
||||
|
||||
filtered = []
|
||||
for message, author, sha in commits:
|
||||
if not any(pattern.search(message) for pattern in compiled_patterns):
|
||||
filtered.append((message, author, sha))
|
||||
for pr in prs:
|
||||
if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
|
||||
filtered.append(pr)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]],
|
||||
cs_commit_new: Optional[str], cs_commit_old: Optional[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],
|
||||
owner: str, repo: str) -> str:
|
||||
"""Generate markdown changelog."""
|
||||
# Calculate statistics
|
||||
commit_count = len(commits)
|
||||
unique_authors = len(set(author for _, author, _ in commits))
|
||||
pr_count = len(prs)
|
||||
unique_authors = len(set(pr["author"] for pr in prs))
|
||||
|
||||
changelog = f"# Dalamud Release v{version}\n\n"
|
||||
changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. "
|
||||
changelog += f"This release includes **{commit_count} commit{'s' if commit_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"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/{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:
|
||||
changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
|
||||
|
|
@ -104,8 +191,8 @@ def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str,
|
|||
|
||||
changelog += "## Dalamud Changes\n\n"
|
||||
|
||||
for message, author, sha in commits:
|
||||
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n"
|
||||
for pr in prs:
|
||||
changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
|
||||
|
||||
return changelog
|
||||
|
||||
|
|
@ -158,11 +245,16 @@ def main():
|
|||
required=True,
|
||||
help="Discord webhook URL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--github-token",
|
||||
default=os.environ.get("GITHUB_TOKEN"),
|
||||
help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Regex patterns to ignore commits (can be specified multiple times)"
|
||||
help="Regex patterns to ignore PRs (can be specified multiple times)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--submodule-path",
|
||||
|
|
@ -172,6 +264,10 @@ def main():
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get repository info
|
||||
owner, repo = get_repo_info()
|
||||
print(f"Repository: {owner}/{repo}")
|
||||
|
||||
# Get the last two tags
|
||||
latest_tag, previous_tag = get_last_two_tags()
|
||||
print(f"Generating changelog between {previous_tag} and {latest_tag}")
|
||||
|
|
@ -185,17 +281,18 @@ def main():
|
|||
if cs_commit_old:
|
||||
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
|
||||
|
||||
# Get commits between tags
|
||||
commits = get_commits_between_tags(latest_tag, previous_tag)
|
||||
print(f"Found {len(commits)} commits")
|
||||
# Get PRs between tags
|
||||
prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
|
||||
prs.reverse()
|
||||
print(f"Found {len(prs)} PRs")
|
||||
|
||||
# Filter commits
|
||||
filtered_commits = filter_commits(commits, args.ignore)
|
||||
print(f"After filtering: {len(filtered_commits)} commits")
|
||||
# Filter PRs
|
||||
filtered_prs = filter_prs(prs, args.ignore)
|
||||
print(f"After filtering: {len(filtered_prs)} PRs")
|
||||
|
||||
# Generate changelog
|
||||
changelog = generate_changelog(latest_tag, previous_tag, filtered_commits,
|
||||
cs_commit_new, cs_commit_old)
|
||||
changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
|
||||
cs_commit_new, cs_commit_old, owner, repo)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("Generated Changelog:")
|
||||
|
|
|
|||
12
.github/workflows/generate-changelog.yml
vendored
12
.github/workflows/generate-changelog.yml
vendored
|
|
@ -6,6 +6,8 @@ on:
|
|||
tags:
|
||||
- '*'
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
generate-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -28,14 +30,14 @@ jobs:
|
|||
pip install requests
|
||||
|
||||
- name: Generate and post changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
run: |
|
||||
python .github/generate_changelog.py \
|
||||
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
|
||||
--ignore "^Merge" \
|
||||
--ignore "^build:" \
|
||||
--ignore "^docs:"
|
||||
env:
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
--ignore "Update ClientStructs" \
|
||||
--ignore "^build:"
|
||||
|
||||
- name: Upload changelog as artifact
|
||||
if: always()
|
||||
|
|
|
|||
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
|
|
@ -1,9 +1,10 @@
|
|||
name: Build Dalamud
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
# Globally blocking because of git pushes in deploy step
|
||||
concurrency:
|
||||
group: build_dalamud_${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
group: build_dalamud_${{ github.repository_owner }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -23,7 +24,7 @@ jobs:
|
|||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9.0.200'
|
||||
dotnet-version: '10.0.100'
|
||||
- name: Define VERSION
|
||||
run: |
|
||||
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,8 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Build Schema",
|
||||
"$ref": "#/definitions/build",
|
||||
"definitions": {
|
||||
"build": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Configuration": {
|
||||
"type": "string",
|
||||
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
|
||||
"enum": [
|
||||
"Debug",
|
||||
"Release"
|
||||
]
|
||||
},
|
||||
"Continue": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates to continue a previously failed build attempt"
|
||||
},
|
||||
"Help": {
|
||||
"type": "boolean",
|
||||
"description": "Shows the help text for this build assembly"
|
||||
},
|
||||
"Host": {
|
||||
"type": "string",
|
||||
"description": "Host for execution. Default is 'automatic'",
|
||||
"enum": [
|
||||
"AppVeyor",
|
||||
"AzurePipelines",
|
||||
|
|
@ -43,9 +21,48 @@
|
|||
"VSCode"
|
||||
]
|
||||
},
|
||||
"IsDocsBuild": {
|
||||
"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": {
|
||||
"Continue": {
|
||||
"type": "boolean",
|
||||
"description": "Whether we are building for documentation - emits generated files"
|
||||
"description": "Indicates to continue a previously failed build attempt"
|
||||
},
|
||||
"Help": {
|
||||
"type": "boolean",
|
||||
"description": "Shows the help text for this build assembly"
|
||||
},
|
||||
"Host": {
|
||||
"description": "Host for execution. Default is 'automatic'",
|
||||
"$ref": "#/definitions/Host"
|
||||
},
|
||||
"NoLogo": {
|
||||
"type": "boolean",
|
||||
|
|
@ -74,65 +91,46 @@
|
|||
"type": "array",
|
||||
"description": "List of targets to be skipped. Empty list skips all dependencies",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CI",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"CompileCImGui",
|
||||
"CompileCImGuizmo",
|
||||
"CompileCImPlot",
|
||||
"CompileDalamud",
|
||||
"CompileDalamudBoot",
|
||||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"CompileInjectorBoot",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
]
|
||||
"$ref": "#/definitions/ExecutableTarget"
|
||||
}
|
||||
},
|
||||
"Solution": {
|
||||
"type": "string",
|
||||
"description": "Path to a solution file that is automatically loaded"
|
||||
},
|
||||
"Target": {
|
||||
"type": "array",
|
||||
"description": "List of targets to be invoked. Default is '{default_target}'",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CI",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"CompileCImGui",
|
||||
"CompileCImGuizmo",
|
||||
"CompileCImPlot",
|
||||
"CompileDalamud",
|
||||
"CompileDalamudBoot",
|
||||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"CompileInjectorBoot",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
]
|
||||
"$ref": "#/definitions/ExecutableTarget"
|
||||
}
|
||||
},
|
||||
"Verbosity": {
|
||||
"type": "string",
|
||||
"description": "Logging verbosity during build execution. Default is 'Normal'",
|
||||
"$ref": "#/definitions/Verbosity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"Configuration": {
|
||||
"type": "string",
|
||||
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
|
||||
"enum": [
|
||||
"Minimal",
|
||||
"Normal",
|
||||
"Quiet",
|
||||
"Verbose"
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
|||
config.LogName = json.value("LogName", config.LogName);
|
||||
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
|
||||
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
|
||||
|
||||
if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
|
||||
config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
|
||||
}
|
||||
|
||||
config.Language = json.value("Language", config.Language);
|
||||
config.Platform = json.value("Platform", config.Platform);
|
||||
config.GameVersion = json.value("GameVersion", config.GameVersion);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ struct DalamudStartInfo {
|
|||
std::string ConfigurationPath;
|
||||
std::string LogPath;
|
||||
std::string LogName;
|
||||
std::string TempDirectory;
|
||||
std::string PluginDirectory;
|
||||
std::string AssetDirectory;
|
||||
ClientLanguage Language = ClientLanguage::English;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
|
||||
|
||||
struct exception_info
|
||||
{
|
||||
LPEXCEPTION_POINTERS pExceptionPointers;
|
||||
|
|
|
|||
|
|
@ -331,6 +331,51 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
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 ==================================== //
|
||||
|
||||
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ HANDLE g_crashhandler_process = nullptr;
|
|||
HANDLE g_crashhandler_event = nullptr;
|
||||
HANDLE g_crashhandler_pipe_write = nullptr;
|
||||
|
||||
wchar_t g_external_event_info[16384] = L"";
|
||||
|
||||
std::recursive_mutex g_exception_handler_mutex;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> g_time_start;
|
||||
|
|
@ -122,6 +124,7 @@ static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstrin
|
|||
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
|
||||
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert<std::wstring>(g_startInfo.TempDirectory) + L"\"");
|
||||
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
|
||||
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
|
||||
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
|
||||
|
|
@ -190,7 +193,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
|
|||
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
|
||||
|
||||
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)";
|
||||
}
|
||||
|
|
@ -251,6 +258,12 @@ LONG WINAPI structured_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)
|
||||
{
|
||||
// pass
|
||||
|
|
@ -434,3 +447,16 @@ bool veh::remove_handler()
|
|||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ namespace veh
|
|||
{
|
||||
bool add_handler(bool doFullDump, const std::string& workingDirectory);
|
||||
bool remove_handler();
|
||||
void raise_external_event(const std::wstring& info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public record DalamudStartInfo
|
|||
/// </summary>
|
||||
public string? ConfigurationPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
|
||||
/// It should also be predictable and easy for launchers to find.
|
||||
/// </summary>
|
||||
public string? TempDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path of the log files.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{8874326B-E755-4D13-90B4-59AB263A3E6B}</ProjectGuid>
|
||||
<RootNamespace>Dalamud_Injector_Boot</RootNamespace>
|
||||
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<TargetName>Dalamud.Injector</TargetName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<OutDir>..\bin\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp23</LanguageStandard>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<ProgramDatabaseFile>$(OutDir)$(TargetName).Boot.pdb</ProgramDatabaseFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>false</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>false</EnableCOMDATFolding>
|
||||
<OptimizeReferences>false</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ItemGroup>
|
||||
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
|
||||
<Link>nethost.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="dalamud.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="resources.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Dalamud.Boot\logging.cpp" />
|
||||
<ClCompile Include="..\Dalamud.Boot\unicode.cpp" />
|
||||
<ClCompile Include="..\lib\CoreCLR\boot.cpp" />
|
||||
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\Dalamud.Boot\logging.h" />
|
||||
<ClInclude Include="..\Dalamud.Boot\unicode.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
|
||||
<Delete Files="$(OutDir)$(TargetName).lib" />
|
||||
<Delete Files="$(OutDir)$(TargetName).exp" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{4faac519-3a73-4b2b-96e7-fb597f02c0be}</UniqueIdentifier>
|
||||
<Extensions>ico;rc</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="dalamud.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="resources.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\lib\CoreCLR\boot.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Dalamud.Boot\logging.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Dalamud.Boot\unicode.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Dalamud.Boot\logging.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Dalamud.Boot\unicode.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
#include <filesystem>
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include "..\Dalamud.Boot\logging.h"
|
||||
#include "..\lib\CoreCLR\CoreCLR.h"
|
||||
#include "..\lib\CoreCLR\boot.h"
|
||||
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
// Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read
|
||||
//logging::start_file_logging("dalamud.injector.boot.log", false);
|
||||
logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
|
||||
logging::I("Built at : " __DATE__ "@" __TIME__);
|
||||
|
||||
wchar_t _module_path[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
|
||||
std::filesystem::path fs_module_path(_module_path);
|
||||
|
||||
std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
|
||||
std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
|
||||
|
||||
// =========================================================================== //
|
||||
|
||||
void* entrypoint_vfn;
|
||||
const auto result = InitializeClrAndGetEntryPoint(
|
||||
GetModuleHandleW(nullptr),
|
||||
false,
|
||||
runtimeconfig_path,
|
||||
module_path,
|
||||
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
|
||||
L"Main",
|
||||
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
|
||||
&entrypoint_vfn);
|
||||
|
||||
if (FAILED(result))
|
||||
return result;
|
||||
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
|
||||
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
|
||||
|
||||
logging::I("Running Dalamud Injector...");
|
||||
const auto ret = entrypoint_fn(argc, argv);
|
||||
logging::I("Done!");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
#pragma once
|
||||
|
|
@ -1 +0,0 @@
|
|||
MAINICON ICON "dalamud.ico"
|
||||
|
|
@ -13,12 +13,13 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Output">
|
||||
<OutputType>Library</OutputType>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputPath>..\bin\$(Configuration)\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<ApplicationIcon>dalamud.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Documentation">
|
||||
|
|
|
|||
|
|
@ -25,34 +25,20 @@ namespace Dalamud.Injector
|
|||
/// <summary>
|
||||
/// Entrypoint to the program.
|
||||
/// </summary>
|
||||
public sealed class EntryPoint
|
||||
public sealed class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
|
||||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">char** string arguments.</param>
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public delegate int MainDelegate(int argc, IntPtr argvPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Start the Dalamud injector.
|
||||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">byte** string arguments.</param>
|
||||
/// <param name="argsArray">Command line arguments.</param>
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public static int Main(int argc, IntPtr argvPtr)
|
||||
public static int Main(string[] argsArray)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> args = new(argc);
|
||||
|
||||
unsafe
|
||||
{
|
||||
var argv = (IntPtr*)argvPtr;
|
||||
for (var i = 0; i < argc; i++)
|
||||
args.Add(Marshal.PtrToStringUni(argv[i]));
|
||||
}
|
||||
// API14 TODO: Refactor
|
||||
var args = argsArray.ToList();
|
||||
args.Insert(0, Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
Init(args);
|
||||
args.Remove("-v"); // Remove "verbose" flag
|
||||
|
|
@ -305,6 +291,7 @@ namespace Dalamud.Injector
|
|||
var configurationPath = startInfo.ConfigurationPath;
|
||||
var pluginDirectory = startInfo.PluginDirectory;
|
||||
var assetDirectory = startInfo.AssetDirectory;
|
||||
var tempDirectory = startInfo.TempDirectory;
|
||||
var delayInitializeMs = startInfo.DelayInitializeMs;
|
||||
var logName = startInfo.LogName;
|
||||
var logPath = startInfo.LogPath;
|
||||
|
|
@ -335,6 +322,10 @@ namespace Dalamud.Injector
|
|||
{
|
||||
assetDirectory = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
|
||||
{
|
||||
tempDirectory = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
|
||||
{
|
||||
delayInitializeMs = int.Parse(args[i][key.Length..]);
|
||||
|
|
@ -447,6 +438,7 @@ namespace Dalamud.Injector
|
|||
startInfo.ConfigurationPath = configurationPath;
|
||||
startInfo.PluginDirectory = pluginDirectory;
|
||||
startInfo.AssetDirectory = assetDirectory;
|
||||
startInfo.TempDirectory = tempDirectory;
|
||||
startInfo.Language = clientLanguage;
|
||||
startInfo.Platform = platform;
|
||||
startInfo.DelayInitializeMs = delayInitializeMs;
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
108
Dalamud.Test/Rpc/DalamudUriTests.cs
Normal file
108
Dalamud.Test/Rpc/DalamudUriTests.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Networking.Rpc.Model;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Rpc
|
||||
{
|
||||
public class DalamudUriTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("https://www.google.com/", false)]
|
||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)]
|
||||
public void ValidatesScheme(string uri, bool valid)
|
||||
{
|
||||
Action act = () => { _ = DalamudUri.FromUri(uri); };
|
||||
|
||||
var ex = Record.Exception(act);
|
||||
if (valid)
|
||||
{
|
||||
Assert.Null(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotNull(ex);
|
||||
Assert.IsType<ArgumentOutOfRangeException>(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")]
|
||||
[InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")]
|
||||
[InlineData("dalamud://Test", "test")]
|
||||
public void ExtractsNamespace(string uri, string expectedNamespace)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(expectedNamespace, dalamudUri.Namespace);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")]
|
||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
||||
[InlineData("dalamud://foo/bar", "/bar")]
|
||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
||||
[InlineData("dalamud://foo/", "/")]
|
||||
public void ExtractsPath(string uri, string expectedPath)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(expectedPath, dalamudUri.Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
||||
[InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar", "/bar")]
|
||||
[InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
||||
[InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")]
|
||||
[InlineData("dalamud://foo/?cow=moo", "/?cow=moo")]
|
||||
[InlineData("dalamud://foo/", "/")]
|
||||
public void ExtractsData(string uri, string expectedData)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
|
||||
Assert.Equal(expectedData, dalamudUri.Data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar", 0)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo", 1)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)]
|
||||
public void ExtractsQueryParams(string uri, int queryCount)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(queryCount, dalamudUri.QueryParams.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh", 5)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux", 4)]
|
||||
[InlineData("dalamud://foo/bar/baz", 3)]
|
||||
[InlineData("dalamud://foo/bar/", 2)]
|
||||
[InlineData("dalamud://foo/bar", 2)]
|
||||
public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
var segments = dalamudUri.Segments;
|
||||
|
||||
// First segment must always be `/`
|
||||
Assert.Equal("/", segments[0]);
|
||||
|
||||
Assert.Equal(segmentCount, segments.Length);
|
||||
|
||||
if (finalSegmentEndsWithSlash)
|
||||
{
|
||||
Assert.EndsWith("/", segments.Last());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Dalamud.sln
14
Dalamud.sln
|
|
@ -1,4 +1,4 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32319.34
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
|
|
@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
tools\BannedSymbols.txt = tools\BannedSymbols.txt
|
||||
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
|
||||
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
|
||||
tools\dalamud.ruleset = tools\dalamud.ruleset
|
||||
Directory.Build.props = Directory.Build.props
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
|
|
@ -27,8 +25,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
|
||||
|
|
@ -49,8 +45,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
|
||||
|
|
@ -103,10 +97,6 @@ Global
|
|||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
|
|
@ -188,8 +178,6 @@ Global
|
|||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
|
||||
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
|
||||
|
|
|
|||
|
|
@ -108,11 +108,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public bool DoPluginTest { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key to opt into Dalamud staging builds.
|
||||
/// </summary>
|
||||
public string? DalamudBetaKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of custom repos.
|
||||
/// </summary>
|
||||
|
|
@ -278,11 +273,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
|
||||
/// </summary>
|
||||
public string? DalamudBetaKind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
|
||||
/// It is reset immediately when read.
|
||||
|
|
@ -497,6 +487,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
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>
|
||||
/// Load a configuration from the provided path.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
|
|||
/// <summary>
|
||||
/// Configuration to store settings for a dalamud plugin.
|
||||
/// </summary>
|
||||
[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
|
||||
[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
|
||||
public sealed class PluginConfigurations
|
||||
{
|
||||
private readonly DirectoryInfo configDirectory;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<PropertyGroup Label="Feature">
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<DalamudVersion>13.0.0.11</DalamudVersion>
|
||||
<DalamudVersion>14.0.0.0</DalamudVersion>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
<FileVersion>$(DalamudVersion)</FileVersion>
|
||||
|
|
@ -73,23 +73,19 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MinSharp" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" />
|
||||
<PackageReference Include="SharpDX.Mathematics" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="sqlite-net-pcl" />
|
||||
<PackageReference Include="StreamJsonRpc" />
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" />
|
||||
<PackageReference Include="System.Resources.Extensions" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -122,6 +118,8 @@
|
|||
<Content Include="licenses.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="Interface\ImGuiBackend\Renderers\gaussian.hlsl" />
|
||||
<None Remove="Interface\ImGuiBackend\Renderers\fullscreen-quad.hlsl.bytes" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -226,9 +224,4 @@
|
|||
<!-- writes the attribute to the customAssemblyInfo file -->
|
||||
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ public enum DalamudAsset
|
|||
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.Font)]
|
||||
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
|
||||
[DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")]
|
||||
FontAwesomeFreeSolid = 2003,
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -53,12 +53,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
DefaultExcelLanguage = this.Language.ToLumina(),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
this.GameData = new(
|
||||
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
|
||||
luminaOptions)
|
||||
{
|
||||
StreamPool = new(),
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Lumina GameData init failed");
|
||||
Util.Fatal(
|
||||
"Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" +
|
||||
"Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".",
|
||||
"Dalamud");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
|
||||
|
||||
|
|
@ -69,8 +82,13 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
|
|||
var tsInfo =
|
||||
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
|
||||
dalamud.StartInfo.TroubleshootingPackData);
|
||||
this.HasModifiedGameDataFiles =
|
||||
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
|
||||
|
||||
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
|
||||
// this.HasModifiedGameDataFiles =
|
||||
// tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
|
||||
|
||||
// TODO: Put above back when check in XL is fixed
|
||||
this.HasModifiedGameDataFiles = false;
|
||||
|
||||
if (this.HasModifiedGameDataFiles)
|
||||
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
|
||||
|
|
|
|||
|
|
@ -192,8 +192,8 @@ public sealed class EntryPoint
|
|||
|
||||
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
|
||||
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
|
||||
Util.GetScmVersion(),
|
||||
Util.GetGitHashClientStructs(),
|
||||
Versioning.GetScmVersion(),
|
||||
Versioning.GetGitHashClientStructs(),
|
||||
FFXIVClientStructs.ThisAssembly.Git.Commits);
|
||||
|
||||
dalamud.WaitForUnload();
|
||||
|
|
@ -263,7 +263,7 @@ public sealed class EntryPoint
|
|||
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
|
||||
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
|
||||
Windows.Win32.PInvoke.SymCleanup(currentProcess);
|
||||
|
|
@ -292,7 +292,6 @@ public sealed class EntryPoint
|
|||
}
|
||||
|
||||
var pluginInfo = string.Empty;
|
||||
var supportText = ", please visit us on Discord for more help";
|
||||
try
|
||||
{
|
||||
var pm = Service<PluginManager>.GetNullable();
|
||||
|
|
@ -300,9 +299,6 @@ public sealed class EntryPoint
|
|||
if (plugin != null)
|
||||
{
|
||||
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
|
||||
|
||||
if (plugin.IsThirdParty)
|
||||
supportText = string.Empty;
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
|
@ -310,31 +306,18 @@ public sealed class EntryPoint
|
|||
// 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();
|
||||
Environment.Exit(-1);
|
||||
|
||||
ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
|
||||
break;
|
||||
default:
|
||||
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
|
||||
|
||||
Log.CloseAndFlush();
|
||||
Environment.Exit(-1);
|
||||
break;
|
||||
}
|
||||
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
namespace Dalamud.Game.Addon;
|
||||
|
||||
/// <summary>Argument pool for Addon Lifecycle services.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class AddonLifecyclePooledArgs : IServiceType
|
||||
{
|
||||
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
|
||||
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
|
||||
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
|
||||
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
|
||||
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
|
||||
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
|
||||
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecyclePooledArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
|
||||
new(out arg, this.addonRequestedUpdateArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
|
||||
new(out arg, this.addonReceiveEventArgPool);
|
||||
|
||||
/// <summary>Returns the object to the pool on dispose.</summary>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
public readonly ref struct PooledEntry<T>
|
||||
where T : AddonArgs, new()
|
||||
{
|
||||
private readonly Span<T> pool;
|
||||
private readonly T obj;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
|
||||
/// <param name="arg">An instance of the argument.</param>
|
||||
/// <param name="pool">The pool to rent from and return to.</param>
|
||||
public PooledEntry(out T arg, Span<T> pool)
|
||||
{
|
||||
this.pool = pool;
|
||||
foreach (ref var item in pool)
|
||||
{
|
||||
if (Interlocked.Exchange(ref item, null) is { } v)
|
||||
{
|
||||
this.obj = arg = v;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.obj = arg = new();
|
||||
}
|
||||
|
||||
/// <summary>Returns the item to the pool.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
var tmp = this.obj;
|
||||
foreach (ref var item in this.pool)
|
||||
{
|
||||
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
|
||||
return;
|
||||
tmp = tmp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ using Dalamud.Logging.Internal;
|
|||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Events;
|
||||
|
|
@ -32,25 +31,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
|
||||
private readonly AddonLifecycleEventListener finalizeEventListener;
|
||||
|
||||
private readonly AddonEventManagerAddressResolver address;
|
||||
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
|
||||
private readonly Hook<AtkUnitManager.Delegates.UpdateCursor> onUpdateCursor;
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
|
||||
|
||||
private AddonCursorType? cursorOverride;
|
||||
private AtkCursor.CursorType? cursorOverride;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonEventManager(TargetSigScanner sigScanner)
|
||||
private AddonEventManager()
|
||||
{
|
||||
this.address = new AddonEventManagerAddressResolver();
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
|
||||
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
|
||||
|
||||
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.addonLifecycle.RegisterListener(this.finalizeEventListener);
|
||||
|
|
@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
this.onUpdateCursor.Enable();
|
||||
}
|
||||
|
||||
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
|
|
@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
/// Force the game cursor to be the specified cursor.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// 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
|
||||
{
|
||||
|
|
@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
|
||||
if (this.cursorOverride is not null && atkStage is not null)
|
||||
{
|
||||
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
|
||||
if (cursor != this.cursorOverride)
|
||||
ref var atkCursor = ref atkStage->AtkCursor;
|
||||
|
||||
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)
|
||||
|
|
@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
|
|||
Log.Error(e, "Exception in UpdateCursorDetour.");
|
||||
}
|
||||
|
||||
return this.onUpdateCursor!.Original(module);
|
||||
this.onUpdateCursor!.Original(thisPtr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||
/// <summary>
|
||||
/// Base class for AddonLifecycle AddonArgTypes.
|
||||
/// </summary>
|
||||
public abstract unsafe class AddonArgs
|
||||
public class AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant string representing the name of an addon that is invalid.
|
||||
/// </summary>
|
||||
public const string InvalidAddon = "NullAddon";
|
||||
|
||||
private string? addonName;
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon this args referrers to.
|
||||
/// </summary>
|
||||
public string AddonName => this.GetAddonName();
|
||||
public string AddonName { get; private set; } = InvalidAddon;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pointer to the addons AtkUnitBase.
|
||||
|
|
@ -25,55 +30,17 @@ public abstract unsafe class AddonArgs
|
|||
public AtkUnitBasePtr Addon
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
internal set
|
||||
{
|
||||
field = value;
|
||||
|
||||
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
|
||||
this.AddonName = value.Name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of these args.
|
||||
/// </summary>
|
||||
public abstract AddonArgsType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if addon name matches the given span of char.
|
||||
/// </summary>
|
||||
/// <param name="name">The name to check.</param>
|
||||
/// <returns>Whether it is the case.</returns>
|
||||
internal bool IsAddon(string name)
|
||||
{
|
||||
if (this.Addon.IsNull)
|
||||
return false;
|
||||
|
||||
if (name.Length is 0 or > 32)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(this.Addon.Name))
|
||||
return false;
|
||||
|
||||
return name == this.Addon.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears this AddonArgs values.
|
||||
/// </summary>
|
||||
internal virtual void Clear()
|
||||
{
|
||||
this.addonName = null;
|
||||
this.Addon = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for ensuring the name of the addon is valid.
|
||||
/// </summary>
|
||||
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
|
||||
private string GetAddonName()
|
||||
{
|
||||
if (this.Addon.IsNull) return InvalidAddon;
|
||||
|
||||
var name = this.Addon.Name;
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return InvalidAddon;
|
||||
|
||||
return this.addonName ??= name;
|
||||
}
|
||||
public virtual AddonArgsType Type => AddonArgsType.Generic;
|
||||
}
|
||||
|
|
|
|||
22
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
Normal file
22
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Close events.
|
||||
/// </summary>
|
||||
public class AddonCloseArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonCloseArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonCloseArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Close;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window should fire the callback method on close.
|
||||
/// </summary>
|
||||
public bool FireCallback { get; set; }
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Draw events.
|
||||
/// </summary>
|
||||
public class AddonDrawArgs : AddonArgs, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonDrawArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Draw;
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for ReceiveEvent events.
|
||||
/// </summary>
|
||||
public class AddonFinalizeArgs : AddonArgs, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonFinalizeArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Finalize;
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
}
|
||||
32
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
Normal file
32
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Hide events.
|
||||
/// </summary>
|
||||
public class AddonHideArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonHideArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonHideArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Hide;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to call the hide callback handler when this hides.
|
||||
/// </summary>
|
||||
public bool CallHideCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flags that the window will set when it Shows/Hides.
|
||||
/// </summary>
|
||||
public uint SetShowHideFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether something for this event message.
|
||||
/// </summary>
|
||||
internal bool UnknownBool { get; set; }
|
||||
}
|
||||
|
|
@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||
/// <summary>
|
||||
/// Addon argument data for ReceiveEvent events.
|
||||
/// </summary>
|
||||
public class AddonReceiveEventArgs : AddonArgs, ICloneable
|
||||
public class AddonReceiveEventArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonReceiveEventArgs()
|
||||
internal AddonReceiveEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -32,23 +31,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
|
|||
public nint AtkEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pointer to a block of data for this event message.
|
||||
/// Gets or sets the pointer to an AtkEventData for this event message.
|
||||
/// </summary>
|
||||
public nint Data { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.AtkEventType = default;
|
||||
this.EventParam = default;
|
||||
this.AtkEvent = default;
|
||||
this.Data = default;
|
||||
}
|
||||
public nint AtkEventData { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Refresh events.
|
||||
/// </summary>
|
||||
public class AddonRefreshArgs : AddonArgs, ICloneable
|
||||
public class AddonRefreshArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonRefreshArgs()
|
||||
internal AddonRefreshArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -31,19 +36,30 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
|
|||
/// <summary>
|
||||
/// Gets the AtkValues in the form of a span.
|
||||
/// </summary>
|
||||
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
|
||||
[Api15ToDo("Make this internal, remove obsolete")]
|
||||
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
/// <summary>
|
||||
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
|
||||
/// </returns>
|
||||
public IEnumerable<AtkValuePtr> AtkValueEnumerable
|
||||
{
|
||||
base.Clear();
|
||||
this.AtkValueCount = default;
|
||||
this.AtkValues = default;
|
||||
get
|
||||
{
|
||||
for (var i = 0; i < this.AtkValueCount; i++)
|
||||
{
|
||||
AtkValuePtr ptr;
|
||||
unsafe
|
||||
{
|
||||
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
|
||||
}
|
||||
|
||||
yield return ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||
/// <summary>
|
||||
/// Addon argument data for OnRequestedUpdate events.
|
||||
/// </summary>
|
||||
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
|
||||
public class AddonRequestedUpdateArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonRequestedUpdateArgs()
|
||||
internal AddonRequestedUpdateArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -25,18 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
|
|||
/// Gets or sets the StringArrayData** for this event.
|
||||
/// </summary>
|
||||
public nint StringArrayData { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.NumberArrayData = default;
|
||||
this.StringArrayData = default;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Setup events.
|
||||
/// </summary>
|
||||
public class AddonSetupArgs : AddonArgs, ICloneable
|
||||
public class AddonSetupArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonSetupArgs()
|
||||
internal AddonSetupArgs()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -31,19 +36,30 @@ public class AddonSetupArgs : AddonArgs, ICloneable
|
|||
/// <summary>
|
||||
/// Gets the AtkValues in the form of a span.
|
||||
/// </summary>
|
||||
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
|
||||
[Api15ToDo("Make this internal, remove obsolete")]
|
||||
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
/// <summary>
|
||||
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
|
||||
/// </returns>
|
||||
public IEnumerable<AtkValuePtr> AtkValueEnumerable
|
||||
{
|
||||
base.Clear();
|
||||
this.AtkValueCount = default;
|
||||
this.AtkValues = default;
|
||||
get
|
||||
{
|
||||
for (var i = 0; i < this.AtkValueCount; i++)
|
||||
{
|
||||
AtkValuePtr ptr;
|
||||
unsafe
|
||||
{
|
||||
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
|
||||
}
|
||||
|
||||
yield return ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
Normal file
27
Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Show events.
|
||||
/// </summary>
|
||||
public class AddonShowArgs : AddonArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonShowArgs"/> class.
|
||||
/// </summary>
|
||||
internal AddonShowArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Show;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window should play open sound effects.
|
||||
/// </summary>
|
||||
public bool SilenceOpenSoundEffect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flags that the window will unset when it Shows/Hides.
|
||||
/// </summary>
|
||||
public uint UnsetShowHideFlags { get; set; }
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for Update events.
|
||||
/// </summary>
|
||||
public class AddonUpdateArgs : AddonArgs, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Not intended for public construction.", false)]
|
||||
public AddonUpdateArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AddonArgsType Type => AddonArgsType.Update;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time since the last update.
|
||||
/// </summary>
|
||||
public float TimeDelta
|
||||
{
|
||||
get => this.TimeDeltaInternal;
|
||||
init => this.TimeDeltaInternal = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time since the last update.
|
||||
/// </summary>
|
||||
internal float TimeDeltaInternal { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
|
||||
|
||||
/// <inheritdoc cref="Clone"/>
|
||||
object ICloneable.Clone() => this.Clone();
|
||||
|
||||
/// <inheritdoc cref="AddonArgs.Clear"/>
|
||||
internal override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.TimeDeltaInternal = default;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,26 +5,16 @@
|
|||
/// </summary>
|
||||
public enum AddonArgsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic arg type that contains no meaningful data.
|
||||
/// </summary>
|
||||
Generic,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Setup.
|
||||
/// </summary>
|
||||
Setup,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Update.
|
||||
/// </summary>
|
||||
Update,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Draw.
|
||||
/// </summary>
|
||||
Draw,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Finalize.
|
||||
/// </summary>
|
||||
Finalize,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for RequestedUpdate.
|
||||
/// </summary>
|
||||
|
|
@ -39,4 +29,19 @@ public enum AddonArgsType
|
|||
/// Contains argument data for ReceiveEvent.
|
||||
/// </summary>
|
||||
ReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Show.
|
||||
/// </summary>
|
||||
Show,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Hide.
|
||||
/// </summary>
|
||||
Hide,
|
||||
|
||||
/// <summary>
|
||||
/// Contains argument data for Close.
|
||||
/// </summary>
|
||||
Close,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ public enum AddonEvent
|
|||
/// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event
|
||||
/// is fired every frame that an addon is loaded, regardless of visibility.
|
||||
/// </summary>
|
||||
/// <seealso cref="AddonUpdateArgs"/>
|
||||
PreUpdate,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -42,7 +41,6 @@ public enum AddonEvent
|
|||
/// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike
|
||||
/// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
|
||||
/// </summary>
|
||||
/// <seealso cref="AddonDrawArgs"/>
|
||||
PreDraw,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -62,7 +60,6 @@ public enum AddonEvent
|
|||
/// <br />
|
||||
/// As this is part of the destruction process for an addon, this event does not have an associated Post event.
|
||||
/// </remarks>
|
||||
/// <seealso cref="AddonFinalizeArgs"/>
|
||||
PreFinalize,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -118,4 +115,92 @@ public enum AddonEvent
|
|||
/// See <see cref="PreReceiveEvent"/> for more information.
|
||||
/// </summary>
|
||||
PostReceiveEvent,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its open method.
|
||||
/// </summary>
|
||||
PreOpen,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its open method.
|
||||
/// </summary>
|
||||
PostOpen,
|
||||
|
||||
/// <summary>
|
||||
/// An even that is fired before an addon processes its Close method.
|
||||
/// </summary>
|
||||
PreClose,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Close method.
|
||||
/// </summary>
|
||||
PostClose,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its Show method.
|
||||
/// </summary>
|
||||
PreShow,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Show method.
|
||||
/// </summary>
|
||||
PostShow,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its Hide method.
|
||||
/// </summary>
|
||||
PreHide,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Hide method.
|
||||
/// </summary>
|
||||
PostHide,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its OnMove method.
|
||||
/// OnMove is triggered only when a move is completed.
|
||||
/// </summary>
|
||||
PreMove,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its OnMove method.
|
||||
/// OnMove is triggered only when a move is completed.
|
||||
/// </summary>
|
||||
PostMove,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its MouseOver method.
|
||||
/// </summary>
|
||||
PreMouseOver,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its MouseOver method.
|
||||
/// </summary>
|
||||
PostMouseOver,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its MouseOut method.
|
||||
/// </summary>
|
||||
PreMouseOut,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its MouseOut method.
|
||||
/// </summary>
|
||||
PostMouseOut,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired before an addon processes its Focus method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
|
||||
/// </remarks>
|
||||
PreFocus,
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired after an addon has processed its Focus method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
|
||||
/// </remarks>
|
||||
PostFocus,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Hooking.Internal;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
|
@ -21,75 +19,36 @@ namespace Dalamud.Game.Addon.Lifecycle;
|
|||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AddonLifecycle : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a list of all allocated addon virtual tables.
|
||||
/// </summary>
|
||||
public static readonly List<AddonVirtualTable> AllocatedTables = [];
|
||||
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
private readonly nint disallowedReceiveEventAddress;
|
||||
|
||||
private readonly AddonLifecycleAddressResolver address;
|
||||
private readonly AddonSetupHook<AtkUnitBase.Delegates.OnSetup> onAddonSetupHook;
|
||||
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
|
||||
private readonly CallHook<AtkUnitBase.Delegates.Draw> onAddonDrawHook;
|
||||
private readonly CallHook<AtkUnitBase.Delegates.Update> onAddonUpdateHook;
|
||||
private readonly Hook<AtkUnitManager.Delegates.RefreshAddon> onAddonRefreshHook;
|
||||
private readonly CallHook<AtkUnitBase.Delegates.OnRequestedUpdate> onAddonRequestedUpdateHook;
|
||||
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecycle(TargetSigScanner sigScanner)
|
||||
private AddonLifecycle()
|
||||
{
|
||||
this.address = new AddonLifecycleAddressResolver();
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
|
||||
|
||||
var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
|
||||
|
||||
this.onAddonSetupHook = new AddonSetupHook<AtkUnitBase.Delegates.OnSetup>(this.address.AddonSetup, this.OnAddonSetup);
|
||||
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
|
||||
this.onAddonDrawHook = new CallHook<AtkUnitBase.Delegates.Draw>(this.address.AddonDraw, this.OnAddonDraw);
|
||||
this.onAddonUpdateHook = new CallHook<AtkUnitBase.Delegates.Update>(this.address.AddonUpdate, this.OnAddonUpdate);
|
||||
this.onAddonRefreshHook = Hook<AtkUnitManager.Delegates.RefreshAddon>.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
|
||||
this.onAddonRequestedUpdateHook = new CallHook<AtkUnitBase.Delegates.OnRequestedUpdate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
|
||||
|
||||
this.onAddonSetupHook.Enable();
|
||||
this.onAddonFinalizeHook.Enable();
|
||||
this.onAddonDrawHook.Enable();
|
||||
this.onAddonUpdateHook.Enable();
|
||||
this.onAddonRefreshHook.Enable();
|
||||
this.onAddonRequestedUpdateHook.Enable();
|
||||
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
|
||||
this.onInitializeAddonHook.Enable();
|
||||
}
|
||||
|
||||
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
|
||||
/// </summary>
|
||||
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all AddonLifecycle Event Listeners.
|
||||
/// </summary>
|
||||
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
|
||||
/// </summary> <br/>
|
||||
/// Mapping is: EventType -> AddonName -> ListenerList
|
||||
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.onAddonSetupHook.Dispose();
|
||||
this.onAddonFinalizeHook.Dispose();
|
||||
this.onAddonDrawHook.Dispose();
|
||||
this.onAddonUpdateHook.Dispose();
|
||||
this.onAddonRefreshHook.Dispose();
|
||||
this.onAddonRequestedUpdateHook.Dispose();
|
||||
this.onInitializeAddonHook?.Dispose();
|
||||
this.onInitializeAddonHook = null;
|
||||
|
||||
foreach (var receiveEventListener in this.ReceiveEventListeners)
|
||||
{
|
||||
receiveEventListener.Dispose();
|
||||
}
|
||||
AllocatedTables.ForEach(entry => entry.Dispose());
|
||||
AllocatedTables.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -98,20 +57,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
|
|||
/// <param name="listener">The listener to register.</param>
|
||||
internal void RegisterListener(AddonLifecycleEventListener listener)
|
||||
{
|
||||
this.framework.RunOnTick(() =>
|
||||
if (!this.EventListeners.ContainsKey(listener.EventType))
|
||||
{
|
||||
this.EventListeners.Add(listener);
|
||||
if (!this.EventListeners.TryAdd(listener.EventType, []))
|
||||
return;
|
||||
}
|
||||
|
||||
// If we want receive event messages have an already active addon, enable the receive event hook.
|
||||
// If the addon isn't active yet, we'll grab the hook when it sets up.
|
||||
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
||||
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
|
||||
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
|
||||
{
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
|
||||
{
|
||||
receiveEventListener.TryEnable();
|
||||
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -120,28 +79,14 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
|
|||
/// <param name="listener">The listener to unregister.</param>
|
||||
internal void UnregisterListener(AddonLifecycleEventListener listener)
|
||||
{
|
||||
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
|
||||
listener.Removed = true;
|
||||
|
||||
this.framework.RunOnTick(() =>
|
||||
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
|
||||
{
|
||||
this.EventListeners.Remove(listener);
|
||||
|
||||
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
|
||||
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
||||
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
|
||||
{
|
||||
// Get the ReceiveEvent Listener for this addon
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
|
||||
{
|
||||
// If there are no other listeners listening for this event, disable the hook.
|
||||
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
|
||||
{
|
||||
receiveEventListener.Disable();
|
||||
addonListener.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke listeners for the specified event type.
|
||||
|
|
@ -151,226 +96,63 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
|
|||
/// <param name="blame">What to blame on errors.</param>
|
||||
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
|
||||
{
|
||||
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
|
||||
foreach (var listener in this.EventListeners)
|
||||
// Early return if we don't have any listeners of this type
|
||||
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
|
||||
|
||||
// Handle listeners for this event type that don't care which addon is triggering it
|
||||
if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
|
||||
{
|
||||
foreach (var listener in globalListeners)
|
||||
{
|
||||
if (listener.EventType != eventType)
|
||||
continue;
|
||||
|
||||
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
|
||||
if (listener.Removed)
|
||||
continue;
|
||||
|
||||
// Match on string.empty for listeners that want events for all addons.
|
||||
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterReceiveEventHook(AtkUnitBase* addon)
|
||||
// Handle listeners that are listening for this addon and event type specifically
|
||||
if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
|
||||
{
|
||||
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
|
||||
// Disallows hooking the core internal event handler.
|
||||
var addonName = addon->NameString;
|
||||
var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
|
||||
if (receiveEventAddress != this.disallowedReceiveEventAddress)
|
||||
{
|
||||
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
|
||||
{
|
||||
if (!existingListener.AddonNames.Contains(addonName))
|
||||
{
|
||||
existingListener.AddonNames.Add(addonName);
|
||||
}
|
||||
}
|
||||
|
||||
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
|
||||
else
|
||||
{
|
||||
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
|
||||
}
|
||||
|
||||
// If we have an active listener for this addon already, we need to activate this hook.
|
||||
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
|
||||
{
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
|
||||
{
|
||||
receiveEventListener.TryEnable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterReceiveEventHook(string addonName)
|
||||
{
|
||||
// Remove this addons ReceiveEvent Registration
|
||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
|
||||
{
|
||||
eventListener.AddonNames.Remove(addonName);
|
||||
|
||||
// If there are no more listeners let's remove and dispose.
|
||||
if (eventListener.AddonNames.Count is 0)
|
||||
{
|
||||
this.ReceiveEventListeners.Remove(eventListener);
|
||||
eventListener.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
foreach (var listener in addonListener)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.RegisterReceiveEventHook(addon);
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
|
||||
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.AtkValueCount = valueCount;
|
||||
arg.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
|
||||
valueCount = arg.AtkValueCount;
|
||||
values = (AtkValue*)arg.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
addon->OnSetup(valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
|
||||
}
|
||||
|
||||
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
||||
private void OnAddonInitialize(AtkUnitBase* addon)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addonName = atkUnitBase[0]->NameString;
|
||||
this.UnregisterReceiveEventHook(addonName);
|
||||
this.LogInitialize(addon->NameString);
|
||||
|
||||
// AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
|
||||
AllocatedTables.Add(new AddonVirtualTable(addon, this));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
|
||||
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
|
||||
}
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)atkUnitBase[0];
|
||||
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
|
||||
this.onInitializeAddonHook!.Original(addon);
|
||||
}
|
||||
|
||||
try
|
||||
[Conditional("DEBUG")]
|
||||
private void LogInitialize(string addonName)
|
||||
{
|
||||
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
|
||||
|
||||
try
|
||||
{
|
||||
addon->Draw();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.TimeDeltaInternal = delta;
|
||||
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
|
||||
|
||||
try
|
||||
{
|
||||
addon->Update(delta);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
|
||||
}
|
||||
|
||||
private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.AtkValueCount = valueCount;
|
||||
arg.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
|
||||
valueCount = arg.AtkValueCount;
|
||||
values = (AtkValue*)arg.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.NumberArrayData = (nint)numberArrayData;
|
||||
arg.StringArrayData = (nint)stringArrayData;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
|
||||
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)arg.StringArrayData;
|
||||
|
||||
try
|
||||
{
|
||||
addon->OnRequestedUpdate(numberArrayData, stringArrayData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
|
||||
Log.Debug($"Initializing {addonName}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +169,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
|
||||
|
||||
private readonly List<AddonLifecycleEventListener> eventListeners = new();
|
||||
private readonly List<AddonLifecycleEventListener> eventListeners = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// AddonLifecycleService memory address resolver.
|
||||
/// </summary>
|
||||
internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
|
||||
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
|
||||
/// This is called for a majority of all addon OnSetup's.
|
||||
/// </summary>
|
||||
public nint AddonSetup { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
|
||||
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
|
||||
/// This seems to be called rarely for specific addons.
|
||||
/// </summary>
|
||||
public nint AddonSetup2 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
|
||||
/// </summary>
|
||||
public nint AddonFinalize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon draw hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonDraw { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon update hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonOnRequestedUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan for and setup any configured address pointers.
|
||||
/// </summary>
|
||||
/// <param name="sig">The signature scanner to facilitate setup.</param>
|
||||
protected override void Setup64Bit(ISigScanner sig)
|
||||
{
|
||||
this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
|
||||
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
|
||||
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
|
||||
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
|
||||
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
|
||||
}
|
||||
}
|
||||
|
|
@ -26,11 +26,6 @@ internal class AddonLifecycleEventListener
|
|||
/// </summary>
|
||||
public string AddonName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this event has been unregistered.
|
||||
/// </summary>
|
||||
public bool Removed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event type this listener is looking for.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
|
||||
/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
|
||||
/// </summary>
|
||||
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="service">AddonLifecycle service instance.</param>
|
||||
/// <param name="addonName">Initial Addon Requesting this listener.</param>
|
||||
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
|
||||
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
|
||||
{
|
||||
this.AddonLifecycle = service;
|
||||
this.AddonNames = [addonName];
|
||||
this.FunctionAddress = receiveEventAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of addons that use this receive event hook.
|
||||
/// </summary>
|
||||
public List<string> AddonNames { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
|
||||
/// </summary>
|
||||
public nint FunctionAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the contained hook for these addons.
|
||||
/// </summary>
|
||||
public Hook<AtkUnitBase.Delegates.ReceiveEvent>? Hook { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Reference to AddonLifecycle service instance.
|
||||
/// </summary>
|
||||
private AddonLifecycle AddonLifecycle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Try to hook and enable this receive event handler.
|
||||
/// </summary>
|
||||
public void TryEnable()
|
||||
{
|
||||
this.Hook ??= Hook<AtkUnitBase.Delegates.ReceiveEvent>.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
|
||||
this.Hook?.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable the hook for this receive event handler.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
this.Hook?.Disable();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Hook?.Dispose();
|
||||
}
|
||||
|
||||
private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
// Check that we didn't get here through a call to another addons handler.
|
||||
var addonName = addon->NameString;
|
||||
if (!this.AddonNames.Contains(addonName))
|
||||
{
|
||||
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
|
||||
return;
|
||||
}
|
||||
|
||||
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
|
||||
arg.Clear();
|
||||
arg.Addon = (nint)addon;
|
||||
arg.AtkEventType = (byte)eventType;
|
||||
arg.EventParam = eventParam;
|
||||
arg.AtkEvent = (IntPtr)atkEvent;
|
||||
arg.Data = (nint)atkEventData;
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
|
||||
eventType = (AtkEventType)arg.AtkEventType;
|
||||
eventParam = arg.EventParam;
|
||||
atkEvent = (AtkEvent*)arg.AtkEvent;
|
||||
atkEventData = (AtkEventData*)arg.Data;
|
||||
|
||||
try
|
||||
{
|
||||
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
using Reloaded.Hooks.Definitions;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
|
||||
internal class AddonSetupHook<T> : IDisposable where T : Delegate
|
||||
{
|
||||
private readonly Reloaded.Hooks.AsmHook asmHook;
|
||||
|
||||
private T? detour;
|
||||
private bool activated;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonSetupHook{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the instruction to replace.</param>
|
||||
/// <param name="detour">Delegate to invoke.</param>
|
||||
internal AddonSetupHook(nint address, T detour)
|
||||
{
|
||||
this.detour = detour;
|
||||
|
||||
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
|
||||
var code = new[]
|
||||
{
|
||||
"use64",
|
||||
$"mov r9, 0x{detourPtr:X8}",
|
||||
};
|
||||
|
||||
var opt = new AsmHookOptions
|
||||
{
|
||||
PreferRelativeJump = true,
|
||||
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
|
||||
MaxOpcodeSize = 5,
|
||||
};
|
||||
|
||||
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the hook is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled => this.asmHook.IsEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Starts intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (!this.activated)
|
||||
{
|
||||
this.activated = true;
|
||||
this.asmHook.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.asmHook.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a hook from the current process.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
this.detour = null;
|
||||
}
|
||||
}
|
||||
638
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
Normal file
638
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a class that holds references to an addons original and modified virtual table entries.
|
||||
/// </summary>
|
||||
internal unsafe class AddonVirtualTable : IDisposable
|
||||
{
|
||||
// This need to be at minimum the largest virtual table size of all addons
|
||||
// Copying extra entries is not problematic, and is considered safe.
|
||||
private const int VirtualTableEntryCount = 200;
|
||||
|
||||
private const bool EnableLogging = false;
|
||||
|
||||
private static readonly ModuleLog Log = new("LifecycleVT");
|
||||
|
||||
private readonly AddonLifecycle lifecycleService;
|
||||
|
||||
// Each addon gets its own set of args that are used to mutate the original call when used in pre-calls
|
||||
private readonly AddonSetupArgs setupArgs = new();
|
||||
private readonly AddonArgs finalizeArgs = new();
|
||||
private readonly AddonArgs drawArgs = new();
|
||||
private readonly AddonArgs updateArgs = new();
|
||||
private readonly AddonRefreshArgs refreshArgs = new();
|
||||
private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new();
|
||||
private readonly AddonReceiveEventArgs receiveEventArgs = new();
|
||||
private readonly AddonArgs openArgs = new();
|
||||
private readonly AddonCloseArgs closeArgs = new();
|
||||
private readonly AddonShowArgs showArgs = new();
|
||||
private readonly AddonHideArgs hideArgs = new();
|
||||
private readonly AddonArgs onMoveArgs = new();
|
||||
private readonly AddonArgs onMouseOverArgs = new();
|
||||
private readonly AddonArgs onMouseOutArgs = new();
|
||||
private readonly AddonArgs focusArgs = new();
|
||||
|
||||
private readonly AtkUnitBase* atkUnitBase;
|
||||
|
||||
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable;
|
||||
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable;
|
||||
|
||||
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
|
||||
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
|
||||
private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
|
||||
private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
|
||||
private readonly AtkUnitBase.Delegates.Draw drawFunction;
|
||||
private readonly AtkUnitBase.Delegates.Update updateFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
|
||||
private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
|
||||
private readonly AtkUnitBase.Delegates.Open openFunction;
|
||||
private readonly AtkUnitBase.Delegates.Close closeFunction;
|
||||
private readonly AtkUnitBase.Delegates.Show showFunction;
|
||||
private readonly AtkUnitBase.Delegates.Hide hideFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnMove onMoveFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
|
||||
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
|
||||
private readonly AtkUnitBase.Delegates.Focus focusFunction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addon">AtkUnitBase* for the addon to replace the table of.</param>
|
||||
/// <param name="lifecycleService">Reference to AddonLifecycle service to callback and invoke listeners.</param>
|
||||
internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
|
||||
{
|
||||
this.atkUnitBase = addon;
|
||||
this.lifecycleService = lifecycleService;
|
||||
|
||||
// Save original virtual table
|
||||
this.originalVirtualTable = addon->VirtualTable;
|
||||
|
||||
// Create copy of original table
|
||||
// Note this will copy any derived/overriden functions that this specific addon has.
|
||||
// Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
|
||||
this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
|
||||
NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
|
||||
// Overwrite the addons existing virtual table with our own
|
||||
addon->VirtualTable = this.modifiedVirtualTable;
|
||||
|
||||
// Pin each of our listener functions
|
||||
this.destructorFunction = this.OnAddonDestructor;
|
||||
this.onSetupFunction = this.OnAddonSetup;
|
||||
this.finalizerFunction = this.OnAddonFinalize;
|
||||
this.drawFunction = this.OnAddonDraw;
|
||||
this.updateFunction = this.OnAddonUpdate;
|
||||
this.onRefreshFunction = this.OnAddonRefresh;
|
||||
this.onRequestedUpdateFunction = this.OnRequestedUpdate;
|
||||
this.onReceiveEventFunction = this.OnAddonReceiveEvent;
|
||||
this.openFunction = this.OnAddonOpen;
|
||||
this.closeFunction = this.OnAddonClose;
|
||||
this.showFunction = this.OnAddonShow;
|
||||
this.hideFunction = this.OnAddonHide;
|
||||
this.onMoveFunction = this.OnAddonMove;
|
||||
this.onMouseOverFunction = this.OnAddonMouseOver;
|
||||
this.onMouseOutFunction = this.OnAddonMouseOut;
|
||||
this.focusFunction = this.OnAddonFocus;
|
||||
|
||||
// Overwrite specific virtual table entries
|
||||
this.modifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
|
||||
this.modifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
|
||||
this.modifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
|
||||
this.modifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
|
||||
this.modifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
|
||||
this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
|
||||
this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
|
||||
this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
|
||||
this.modifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
|
||||
this.modifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
|
||||
this.modifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
|
||||
this.modifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
|
||||
this.modifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
|
||||
this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
|
||||
this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
|
||||
this.modifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// Ensure restoration is done atomically.
|
||||
Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable);
|
||||
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
}
|
||||
|
||||
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
|
||||
{
|
||||
AtkEventListener* result = null;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
try
|
||||
{
|
||||
result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
if ((freeFlags & 1) == 1)
|
||||
{
|
||||
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
|
||||
AddonLifecycle.AllocatedTables.Remove(this);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.setupArgs.Addon = addon;
|
||||
this.setupArgs.AtkValueCount = valueCount;
|
||||
this.setupArgs.AtkValues = (nint)values;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs);
|
||||
|
||||
valueCount = this.setupArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.setupArgs.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->OnSetup(addon, valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonFinalize(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.finalizeArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->Finalizer(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.drawArgs.Addon = addon;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->Draw(addon);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.updateArgs.Addon = addon;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs);
|
||||
|
||||
// Note: Do not pass or allow manipulation of delta.
|
||||
// It's realistically not something that should be needed.
|
||||
// And even if someone does, they are encouraged to hook Update themselves.
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->Update(addon, delta);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.refreshArgs.Addon = addon;
|
||||
this.refreshArgs.AtkValueCount = valueCount;
|
||||
this.refreshArgs.AtkValues = (nint)values;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs);
|
||||
|
||||
valueCount = this.refreshArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.refreshArgs.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.originalVirtualTable->OnRefresh(addon, valueCount, values);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.requestedUpdateArgs.Addon = addon;
|
||||
this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
|
||||
this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs);
|
||||
|
||||
numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData;
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.receiveEventArgs.Addon = (nint)addon;
|
||||
this.receiveEventArgs.AtkEventType = (byte)eventType;
|
||||
this.receiveEventArgs.EventParam = eventParam;
|
||||
this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent;
|
||||
this.receiveEventArgs.AtkEventData = (nint)atkEventData;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs);
|
||||
|
||||
eventType = (AtkEventType)this.receiveEventArgs.AtkEventType;
|
||||
eventParam = this.receiveEventArgs.EventParam;
|
||||
atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent;
|
||||
atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData;
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.openArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs);
|
||||
|
||||
try
|
||||
{
|
||||
result = this.originalVirtualTable->Open(thisPtr, depthLayer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.closeArgs.Addon = thisPtr;
|
||||
this.closeArgs.FireCallback = fireCallback;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs);
|
||||
|
||||
fireCallback = this.closeArgs.FireCallback;
|
||||
|
||||
try
|
||||
{
|
||||
result = this.originalVirtualTable->Close(thisPtr, fireCallback);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.showArgs.Addon = thisPtr;
|
||||
this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect;
|
||||
this.showArgs.UnsetShowHideFlags = unsetShowHideFlags;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs);
|
||||
|
||||
silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect;
|
||||
unsetShowHideFlags = this.showArgs.UnsetShowHideFlags;
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.hideArgs.Addon = thisPtr;
|
||||
this.hideArgs.UnknownBool = unkBool;
|
||||
this.hideArgs.CallHideCallback = callHideCallback;
|
||||
this.hideArgs.SetShowHideFlags = setShowHideFlags;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs);
|
||||
|
||||
unkBool = this.hideArgs.UnknownBool;
|
||||
callHideCallback = this.hideArgs.CallHideCallback;
|
||||
setShowHideFlags = this.hideArgs.SetShowHideFlags;
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonMove(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.onMoveArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->OnMove(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonMouseOver(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.onMouseOverArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->OnMouseOver(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonMouseOut(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.onMouseOutArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->OnMouseOut(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonFocus(AtkUnitBase* thisPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogEvent(EnableLogging);
|
||||
|
||||
this.focusArgs.Addon = thisPtr;
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs);
|
||||
|
||||
try
|
||||
{
|
||||
this.originalVirtualTable->Focus(thisPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus.");
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
|
||||
{
|
||||
if (loggingEnabled)
|
||||
{
|
||||
// Manually disable the really spammy log events, you can comment this out if you need to debug them.
|
||||
if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
|
||||
return;
|
||||
|
||||
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
|
||||
if (this.configuration.PrintDalamudWelcomeMsg)
|
||||
{
|
||||
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
|
||||
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion())
|
||||
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
|
||||
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion))
|
||||
{
|
||||
var linkPayload = chatGui.AddChatLinkHandler(
|
||||
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
|
||||
|
|
@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType
|
|||
Type = XivChatType.Notice,
|
||||
});
|
||||
|
||||
this.configuration.LastVersion = Util.AssemblyVersion;
|
||||
this.configuration.LastVersion = Versioning.GetAssemblyVersion();
|
||||
this.configuration.QueueSave();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,47 +63,37 @@ public interface IAetheryteEntry
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing an aetheryte entry available to the game.
|
||||
/// </summary>
|
||||
internal sealed class AetheryteEntry : IAetheryteEntry
|
||||
{
|
||||
private readonly TeleportInfo data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
|
||||
/// This struct represents an aetheryte entry available to the game.
|
||||
/// </summary>
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal AetheryteEntry(TeleportInfo data)
|
||||
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
|
||||
{
|
||||
this.data = data;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => data.AetheryteId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => this.data.AetheryteId;
|
||||
public uint TerritoryId => data.TerritoryId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint TerritoryId => this.data.TerritoryId;
|
||||
public byte SubIndex => data.SubIndex;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte SubIndex => this.data.SubIndex;
|
||||
public byte Ward => data.Ward;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Ward => this.data.Ward;
|
||||
public byte Plot => data.Plot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Plot => this.data.Plot;
|
||||
public uint GilCost => data.GilCost;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint GilCost => this.data.GilCost;
|
||||
public bool IsFavourite => data.IsFavourite;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsFavourite => this.data.IsFavourite;
|
||||
public bool IsSharedHouse => data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSharedHouse => this.data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsApartment => this.data.IsApartment;
|
||||
public bool IsApartment => data.IsApartment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);
|
||||
|
|
|
|||
|
|
@ -88,10 +88,7 @@ internal sealed partial class AetheryteList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IAetheryteEntry> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -99,4 +96,34 @@ internal sealed partial class AetheryteList
|
|||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
private struct Enumerator(AetheryteList aetheryteList) : IEnumerator<IAetheryteEntry>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IAetheryteEntry Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (++this.index < aetheryteList.Length)
|
||||
{
|
||||
this.Current = aetheryteList[this.index];
|
||||
return true;
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
|
@ -23,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy;
|
|||
#pragma warning restore SA1015
|
||||
internal sealed partial class BuddyList : IServiceType, IBuddyList
|
||||
{
|
||||
private const uint InvalidObjectID = 0xE0000000;
|
||||
private const uint InvalidEntityId = 0xE0000000;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly PlayerState playerState = Service<PlayerState>.Get();
|
||||
|
|
@ -84,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetCompanionBuddyMemberAddress()
|
||||
public unsafe nint GetCompanionBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetPetBuddyMemberAddress()
|
||||
public unsafe nint GetPetBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
|
||||
return (nint)this.BuddyListStruct->PetInfo.Pet;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
|
||||
public unsafe nint GetBattleBuddyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= 3)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
|
||||
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
if (!this.playerState.IsLoaded)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
var buddy = new BuddyMember(address);
|
||||
if (buddy.ObjectId == InvalidObjectID)
|
||||
var buddy = new BuddyMember((CSBuddyMember*)address);
|
||||
if (buddy.EntityId == InvalidEntityId)
|
||||
return null;
|
||||
|
||||
return buddy;
|
||||
|
|
@ -132,12 +133,39 @@ internal sealed partial class BuddyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IBuddyMember> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(BuddyList buddyList) : IEnumerator<IBuddyMember>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IBuddyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (++this.index < buddyList.Length)
|
||||
{
|
||||
this.Current = buddyList[this.index];
|
||||
return true;
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
public interface IBuddyMember
|
||||
public interface IBuddyMember : IEquatable<IBuddyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the buddy in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object ID of this buddy.
|
||||
|
|
@ -67,42 +71,34 @@ public interface IBuddyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
internal unsafe class BuddyMember : IBuddyMember
|
||||
/// <param name="ptr">A pointer to the BuddyMember.</param>
|
||||
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuddyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Buddy address.</param>
|
||||
internal BuddyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
public uint ObjectId => this.EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint EntityId => this.Struct->EntityId;
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
|
||||
public uint CurrentHP => ptr->CurrentHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint CurrentHP => this.Struct->CurrentHealth;
|
||||
public uint MaxHP => ptr->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint MaxHP => this.Struct->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint DataID => this.Struct->DataId;
|
||||
public uint DataID => ptr->DataId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
|
||||
|
|
@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
|
|||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
|
||||
public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IBuddyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is BuddyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.ClientState;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Data;
|
||||
|
|
@ -7,10 +8,12 @@ using Dalamud.Memory;
|
|||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Fates;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing an fate entry that can be seen in the current area.
|
||||
/// Interface representing a fate entry that can be seen in the current area.
|
||||
/// </summary>
|
||||
public interface IFate : IEquatable<IFate>
|
||||
{
|
||||
|
|
@ -112,129 +115,96 @@ public interface IFate : IEquatable<IFate>
|
|||
/// <summary>
|
||||
/// Gets the address of this Fate in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents an FFXIV Fate.
|
||||
/// This struct represents a Fate.
|
||||
/// </summary>
|
||||
internal unsafe partial class Fate
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Fate"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of this fate in memory.</param>
|
||||
internal Fate(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
|
||||
|
||||
public static bool operator ==(Fate fate1, Fate fate2)
|
||||
{
|
||||
if (fate1 is null || fate2 is null)
|
||||
return Equals(fate1, fate2);
|
||||
|
||||
return fate1.Equals(fate2);
|
||||
}
|
||||
|
||||
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this Fate is still valid in memory.
|
||||
/// </summary>
|
||||
/// <param name="fate">The fate to check.</param>
|
||||
/// <returns>True or false.</returns>
|
||||
public static bool IsValid(Fate fate)
|
||||
{
|
||||
if (fate == null)
|
||||
return false;
|
||||
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
return playerState.IsLoaded == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this actor is still valid in memory.
|
||||
/// </summary>
|
||||
/// <returns>True or false.</returns>
|
||||
public bool IsValid() => IsValid(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IEquatable<IFate>.Equals(IFate other) => this.FateId == other?.FateId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => ((IEquatable<IFate>)this).Equals(obj as IFate);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.FateId.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents an FFXIV Fate.
|
||||
/// </summary>
|
||||
internal unsafe partial class Fate : IFate
|
||||
/// <param name="ptr">A pointer to the FateContext.</param>
|
||||
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ushort FateId => this.Struct->FateId;
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort FateId => ptr->FateId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
|
||||
public int StartTimeEpoch => ptr->StartTimeEpoch;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public short Duration => this.Struct->Duration;
|
||||
public short Duration => ptr->Duration;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
|
||||
public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
|
||||
public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
|
||||
public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FateState State => (FateState)this.Struct->State;
|
||||
public FateState State => (FateState)ptr->State;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte HandInCount => this.Struct->HandInCount;
|
||||
public byte HandInCount => ptr->HandInCount;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Progress => this.Struct->Progress;
|
||||
public byte Progress => ptr->Progress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasBonus => this.Struct->IsBonus;
|
||||
public bool HasBonus => ptr->IsBonus;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint IconId => this.Struct->IconId;
|
||||
public uint IconId => ptr->IconId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => ptr->Level;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte MaxLevel => this.Struct->MaxLevel;
|
||||
public byte MaxLevel => ptr->MaxLevel;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Vector3 Position => this.Struct->Location;
|
||||
public Vector3 Position => ptr->Location;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float Radius => this.Struct->Radius;
|
||||
public float Radius => ptr->Radius;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MapIconId => this.Struct->MapIconId;
|
||||
public uint MapIconId => ptr->MapIconId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the territory this <see cref="Fate"/> is located in.
|
||||
/// </summary>
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId);
|
||||
|
||||
public static bool operator ==(Fate x, Fate y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(Fate x, Fate y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IFate? other)
|
||||
{
|
||||
return this.FateId == other.FateId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is Fate fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.FateId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Dalamud.IoC;
|
|||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
|
||||
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Fates;
|
||||
|
|
@ -26,7 +27,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
|
||||
public unsafe nint Address => (nint)CSFateManager.Instance();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe int Length
|
||||
|
|
@ -69,29 +70,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetFateAddress(int index)
|
||||
public unsafe nint GetFateAddress(int index)
|
||||
{
|
||||
if (index >= this.Length)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
var fateManager = CSFateManager.Instance();
|
||||
if (fateManager == null)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)fateManager->Fates[index].Value;
|
||||
return (nint)fateManager->Fates[index].Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFate? CreateFateReference(IntPtr offset)
|
||||
public unsafe IFate? CreateFateReference(IntPtr address)
|
||||
{
|
||||
if (offset == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
if (!playerState.IsLoaded)
|
||||
var clientState = Service<ClientState>.Get();
|
||||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
return new Fate(offset);
|
||||
return new Fate((CSFateContext*)address);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,12 +107,39 @@ internal sealed partial class FateTable
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IFate> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(FateTable fateTable) : IEnumerator<IFate>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IFate Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (++this.index < fateTable.Length)
|
||||
{
|
||||
this.Current = fateTable[this.index];
|
||||
return true;
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ using Dalamud.Utility;
|
|||
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
|
||||
|
||||
|
|
@ -37,8 +35,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
|
||||
private readonly CachedEntry[] cachedObjectTable;
|
||||
|
||||
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private unsafe ObjectTable()
|
||||
{
|
||||
|
|
@ -48,9 +44,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
this.cachedObjectTable = new CachedEntry[objectTableLength];
|
||||
for (var i = 0; i < this.cachedObjectTable.Length; i++)
|
||||
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
|
||||
|
||||
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
|
||||
this.frameworkThreadEnumerators[i] = new(this, i);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -243,43 +236,25 @@ internal sealed partial class ObjectTable
|
|||
public IEnumerator<IGameObject> GetEnumerator()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
// If we're on the framework thread, see if there's an already allocated enumerator available for use.
|
||||
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
var t = x;
|
||||
x = null;
|
||||
t.Reset();
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// No reusable enumerator is available; allocate a new temporary one.
|
||||
return new Enumerator(this, -1);
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable
|
||||
private struct Enumerator(ObjectTable owner) : IEnumerator<IGameObject>
|
||||
{
|
||||
private ObjectTable? owner = owner;
|
||||
|
||||
private int index = -1;
|
||||
|
||||
public IGameObject Current { get; private set; } = null!;
|
||||
public IGameObject Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == objectTableLength)
|
||||
return false;
|
||||
var cache = owner.cachedObjectTable.AsSpan();
|
||||
|
||||
var cache = this.owner!.cachedObjectTable.AsSpan();
|
||||
for (this.index++; this.index < objectTableLength; this.index++)
|
||||
while (++this.index < objectTableLength)
|
||||
{
|
||||
if (cache[this.index].Update() is { } ao)
|
||||
{
|
||||
|
|
@ -288,24 +263,17 @@ internal sealed partial class ObjectTable
|
|||
}
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset() => this.index = -1;
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.owner is not { } o)
|
||||
return;
|
||||
|
||||
if (slotId != -1)
|
||||
o.frameworkThreadEnumerators[slotId] = this;
|
||||
}
|
||||
|
||||
public bool TryReset()
|
||||
{
|
||||
this.Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
|
||||
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Party;
|
||||
|
||||
|
|
@ -43,20 +44,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
|
|||
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
|
||||
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
|
||||
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
|
||||
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
|
||||
|
||||
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
|
||||
private static int PartyMemberSize { get; } = Marshal.SizeOf<CSPartyMember>();
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
|
||||
private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? this[int index]
|
||||
|
|
@ -81,39 +82,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GetPartyMemberAddress(int index)
|
||||
public nint GetPartyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= GroupLength)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return this.GroupListAddress + (index * PartyMemberSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? CreatePartyMemberReference(IntPtr address)
|
||||
public IPartyMember? CreatePartyMemberReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember(address);
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember((CSPartyMember*)address);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GetAllianceMemberAddress(int index)
|
||||
public nint GetAllianceMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= AllianceLength)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return this.AllianceListAddress + (index * PartyMemberSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? CreateAllianceMemberReference(IntPtr address)
|
||||
public IPartyMember? CreateAllianceMemberReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember(address);
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember((CSPartyMember*)address);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,18 +135,43 @@ internal sealed partial class PartyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IPartyMember> GetEnumerator()
|
||||
{
|
||||
// Normally using Length results in a recursion crash, however we know the party size via ptr.
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
var member = this[i];
|
||||
|
||||
if (member == null)
|
||||
break;
|
||||
|
||||
yield return member;
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(PartyList partyList) : IEnumerator<IPartyMember>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IPartyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
while (++this.index < partyList.Length)
|
||||
{
|
||||
var partyMember = partyList[this.index];
|
||||
if (partyMember != null)
|
||||
{
|
||||
this.Current = partyMember;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Statuses;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Party;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a party member.
|
||||
/// </summary>
|
||||
public interface IPartyMember
|
||||
public interface IPartyMember : IEquatable<IPartyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of this party member in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of buffs or debuffs applied to this party member.
|
||||
|
|
@ -108,69 +109,81 @@ public interface IPartyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a party member in the group manager.
|
||||
/// This struct represents a party member in the group manager.
|
||||
/// </summary>
|
||||
internal unsafe class PartyMember : IPartyMember
|
||||
/// <param name="ptr">A pointer to the PartyMember.</param>
|
||||
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PartyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the party member.</param>
|
||||
internal PartyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Address { get; }
|
||||
public StatusList Statuses => new(&ptr->StatusManager);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public StatusList Statuses => new(&this.Struct->StatusManager);
|
||||
public Vector3 Position => ptr->Position;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Vector3 Position => this.Struct->Position;
|
||||
public long ContentId => (long)ptr->ContentId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long ContentId => (long)this.Struct->ContentId;
|
||||
public uint ObjectId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint EntityId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint CurrentHP => this.Struct->CurrentHP;
|
||||
public uint CurrentHP => ptr->CurrentHP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MaxHP => this.Struct->MaxHP;
|
||||
public uint MaxHP => ptr->MaxHP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort CurrentMP => this.Struct->CurrentMP;
|
||||
public ushort CurrentMP => ptr->CurrentMP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort MaxMP => this.Struct->MaxMP;
|
||||
public ushort MaxMP => ptr->MaxMP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->TerritoryType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
|
||||
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(ptr->HomeWorld);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Name => SeString.Parse(this.Struct->Name);
|
||||
public SeString Name => SeString.Parse(ptr->Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Sex => this.Struct->Sex;
|
||||
public byte Sex => ptr->Sex;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
|
||||
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(ptr->ClassJob);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => ptr->Level;
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
|
||||
public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(PartyMember x, PartyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IPartyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is PartyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,49 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Statuses;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a status effect an actor is afflicted by.
|
||||
/// Interface representing a status.
|
||||
/// </summary>
|
||||
public unsafe class Status
|
||||
public interface IStatus : IEquatable<IStatus>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Status"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Status address.</param>
|
||||
internal Status(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the status in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status ID of this status.
|
||||
/// </summary>
|
||||
public uint StatusId => this.Struct->StatusId;
|
||||
uint StatusId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GameData associated with this status.
|
||||
/// </summary>
|
||||
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
|
||||
RowRef<Lumina.Excel.Sheets.Status> GameData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value of the status.
|
||||
/// </summary>
|
||||
public ushort Param => this.Struct->Param;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stack count of this status.
|
||||
/// Only valid if this is a non-food status.
|
||||
/// </summary>
|
||||
[Obsolete($"Replaced with {nameof(Param)}", true)]
|
||||
public byte StackCount => (byte)this.Struct->Param;
|
||||
ushort Param { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time remaining of this status.
|
||||
/// </summary>
|
||||
public float RemainingTime => this.Struct->RemainingTime;
|
||||
float RemainingTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source ID of this status.
|
||||
/// </summary>
|
||||
public uint SourceId => this.Struct->SourceObject.ObjectId;
|
||||
uint SourceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source actor associated with this status.
|
||||
|
|
@ -63,7 +51,55 @@ public unsafe class Status
|
|||
/// <remarks>
|
||||
/// This iterates the actor table, it should be used with care.
|
||||
/// </remarks>
|
||||
IGameObject? SourceObject { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents a status effect an actor is afflicted by.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the Status.</param>
|
||||
internal unsafe readonly struct Status(CSStatus* ptr) : IStatus
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint StatusId => ptr->StatusId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(ptr->StatusId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Param => ptr->Param;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float RemainingTime => ptr->RemainingTime;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint SourceId => ptr->SourceObject.ObjectId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
|
||||
public static bool operator ==(Status x, Status y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(Status x, Status y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IStatus? other)
|
||||
{
|
||||
return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is Status fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Player;
|
||||
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Statuses;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ public sealed unsafe partial class StatusList
|
|||
/// Initializes a new instance of the <see cref="StatusList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the status list.</param>
|
||||
internal StatusList(IntPtr address)
|
||||
internal StatusList(nint address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
|
@ -26,14 +26,14 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="pointer">Pointer to the status list.</param>
|
||||
internal unsafe StatusList(void* pointer)
|
||||
: this((IntPtr)pointer)
|
||||
: this((nint)pointer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the status list in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of status effect slots the actor has.
|
||||
|
|
@ -49,7 +49,7 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="index">Status Index.</param>
|
||||
/// <returns>The status at the specified index.</returns>
|
||||
public Status? this[int index]
|
||||
public IStatus? this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -66,7 +66,7 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="address">The address of the status list in memory.</param>
|
||||
/// <returns>The status object containing the requested data.</returns>
|
||||
public static StatusList? CreateStatusListReference(IntPtr address)
|
||||
public static StatusList? CreateStatusListReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero)
|
||||
return null;
|
||||
|
|
@ -74,8 +74,12 @@ public sealed unsafe partial class StatusList
|
|||
// The use case for CreateStatusListReference and CreateStatusReference to be static is so
|
||||
// fake status lists can be generated. Since they aren't exposed as services, it's either
|
||||
// here or somewhere else.
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
if (!playerState.IsLoaded)
|
||||
var clientState = Service<ClientState>.Get();
|
||||
|
||||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new StatusList(address);
|
||||
|
|
@ -86,16 +90,15 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="address">The address of the status effect in memory.</param>
|
||||
/// <returns>The status object containing the requested data.</returns>
|
||||
public static Status? CreateStatusReference(IntPtr address)
|
||||
public static IStatus? CreateStatusReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
if (!playerState.IsLoaded)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new Status(address);
|
||||
return new Status((CSStatus*)address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,22 +106,22 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="index">The index of the status.</param>
|
||||
/// <returns>The memory address of the status.</returns>
|
||||
public IntPtr GetStatusAddress(int index)
|
||||
public nint GetStatusAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= this.Length)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This collection represents the status effects an actor is afflicted by.
|
||||
/// </summary>
|
||||
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
|
||||
public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollection
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
int IReadOnlyCollection<Status>.Count => this.Length;
|
||||
int IReadOnlyCollection<IStatus>.Count => this.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
int ICollection.Count => this.Length;
|
||||
|
|
@ -130,17 +133,9 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
|
|||
object ICollection.SyncRoot => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<Status> GetEnumerator()
|
||||
public IEnumerator<IStatus> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
var status = this[i];
|
||||
|
||||
if (status == null || status.StatusId == 0)
|
||||
continue;
|
||||
|
||||
yield return status;
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -155,4 +150,38 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
|
|||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private struct Enumerator(StatusList statusList) : IEnumerator<IStatus>
|
||||
{
|
||||
private int index = -1;
|
||||
|
||||
public IStatus Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
while (++this.index < statusList.Length)
|
||||
{
|
||||
var status = statusList[this.index];
|
||||
if (status != null && status.StatusId != 0)
|
||||
{
|
||||
this.Current = status;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Structs;
|
||||
|
||||
/// <summary>
|
||||
/// Native memory representation of a FFXIV status effect.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The effect ID.
|
||||
/// </summary>
|
||||
public short EffectId;
|
||||
|
||||
/// <summary>
|
||||
/// How many stacks are present.
|
||||
/// </summary>
|
||||
public byte StackCount;
|
||||
|
||||
/// <summary>
|
||||
/// Additional parameters.
|
||||
/// </summary>
|
||||
public byte Param;
|
||||
|
||||
/// <summary>
|
||||
/// The duration remaining.
|
||||
/// </summary>
|
||||
public float Duration;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the actor that caused this effect.
|
||||
/// </summary>
|
||||
public int OwnerId;
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Dalamud.Game.Config;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Game config system address resolver.
|
||||
|
|
|
|||
|
|
@ -4069,6 +4069,13 @@ public enum UiConfigOption
|
|||
[GameConfigOption("GposePortraitRotateType", ConfigType.UInt)]
|
||||
GposePortraitRotateType,
|
||||
|
||||
/// <summary>
|
||||
/// UiConfig option with the internal name GroupPosePortraitUnlockAspectLimit.
|
||||
/// This option is a UInt.
|
||||
/// </summary>
|
||||
[GameConfigOption("GroupPosePortraitUnlockAspectLimit", ConfigType.UInt)]
|
||||
GroupPosePortraitUnlockAspectLimit,
|
||||
|
||||
/// <summary>
|
||||
/// UiConfig option with the internal name LsListSortPriority.
|
||||
/// This option is a UInt.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.DutyState;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api13ToDo("Maybe make this config scoped to internal name?")]
|
||||
[Api14ToDo("Maybe make this config scoped to internal name?")]
|
||||
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -92,34 +92,16 @@ public enum FlyTextKind : int
|
|||
/// </summary>
|
||||
IslandExp = 15,
|
||||
|
||||
/// <summary>
|
||||
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
|
||||
/// </summary>
|
||||
[Obsolete("Use Dataset instead", true)]
|
||||
Unknown16 = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
|
||||
/// </summary>
|
||||
Dataset = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Val1 in serif font, Text2 in sans-serif as subtitle.
|
||||
/// </summary>
|
||||
[Obsolete("Use Knowledge instead", true)]
|
||||
Unknown17 = 17,
|
||||
|
||||
/// <summary>
|
||||
/// Val1 in serif font, Text2 in sans-serif as subtitle.
|
||||
/// </summary>
|
||||
Knowledge = 17,
|
||||
|
||||
/// <summary>
|
||||
/// Val1 in serif font, Text2 in sans-serif as subtitle.
|
||||
/// </summary>
|
||||
[Obsolete("Use PhantomExp instead", true)]
|
||||
Unknown18 = 18,
|
||||
|
||||
/// <summary>
|
||||
/// Val1 in serif font, Text2 in sans-serif as subtitle.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Gui;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Gui.NamePlate;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
|
|||
private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
// 3 == Close
|
||||
if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
|
||||
if (eventType == AtkEventType.InputReceived && WindowSystem.ShouldInhibitAtkCloseEvents && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
|
||||
{
|
||||
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
|
||||
return;
|
||||
|
|
@ -124,7 +124,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
|
|||
|
||||
private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize)
|
||||
{
|
||||
if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled)
|
||||
if (WindowSystem.ShouldInhibitAtkCloseEvents && this.configuration.IsFocusManagementEnabled)
|
||||
{
|
||||
Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -305,7 +305,8 @@ internal class GameInventory : IInternalDisposableService
|
|||
private GameInventoryItem[] CreateItemsArray(int length)
|
||||
{
|
||||
var items = new GameInventoryItem[length];
|
||||
items.Initialize();
|
||||
foreach (ref var item in items.AsSpan())
|
||||
item = new();
|
||||
return items;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Network;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Dalamud.Game.Network.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Network.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Internal address resolver for the network handlers.
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState
|
|||
public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default;
|
||||
|
||||
/// <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/>
|
||||
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ using System.Runtime.CompilerServices;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using Iced.Intel;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using System.Globalization;
|
|||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
||||
using LSeString = Lumina.Text.SeString;
|
||||
|
||||
namespace Dalamud.Game.Text.Evaluator;
|
||||
|
||||
|
|
@ -71,9 +70,6 @@ public readonly struct SeStringParameter
|
|||
|
||||
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(string value) => new(value);
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ internal record struct NounParams()
|
|||
/// </summary>
|
||||
public readonly int ColumnOffset => this.SheetName switch
|
||||
{
|
||||
// See "E8 ?? ?? ?? ?? 44 8B 6B 08"
|
||||
nameof(LSheets.BeastTribe) => 10,
|
||||
// See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8"
|
||||
nameof(LSheets.BeastTribe) => 11,
|
||||
nameof(LSheets.DeepDungeonItem) => 1,
|
||||
nameof(LSheets.DeepDungeonEquipment) => 1,
|
||||
nameof(LSheets.DeepDungeonMagicStone) => 1,
|
||||
|
|
|
|||
|
|
@ -113,14 +113,6 @@ public class SeString
|
|||
/// <returns>Equivalent SeString.</returns>
|
||||
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>
|
||||
/// Parse a binary game message into an SeString.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")]
|
||||
|
||||
// ImRAII stuff
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")]
|
||||
|
|
|
|||
|
|
@ -201,19 +201,19 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
|
|||
if (EnvironmentConfiguration.DalamudForceMinHook)
|
||||
useMinHook = true;
|
||||
|
||||
using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
|
||||
if (moduleHandle.IsInvalid)
|
||||
var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
|
||||
if (moduleHandle.IsNull)
|
||||
throw new Exception($"Could not get a handle to module {moduleName}");
|
||||
|
||||
var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
|
||||
if (procAddress == IntPtr.Zero)
|
||||
var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
|
||||
if (procAddress.IsNull)
|
||||
throw new Exception($"Could not get the address of {moduleName}::{exportName}");
|
||||
|
||||
procAddress = HookManager.FollowJmp(procAddress);
|
||||
var address = HookManager.FollowJmp(procAddress.Value);
|
||||
if (useMinHook)
|
||||
return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly());
|
||||
return new MinHookHook<T>(address, detour, Assembly.GetCallingAssembly());
|
||||
else
|
||||
return new ReloadedHook<T>(procAddress, detour, Assembly.GetCallingAssembly());
|
||||
return new ReloadedHook<T>(address, detour, Assembly.GetCallingAssembly());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
using Reloaded.Hooks.Definitions;
|
||||
|
||||
namespace Dalamud.Hooking.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
|
||||
/// This is a destructive operation, no other callsite hooks can coexist at the same address.
|
||||
///
|
||||
/// There's no .Original for this hook type.
|
||||
/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
|
||||
///
|
||||
/// This class was specifically added for hooking virtual function callsites.
|
||||
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
|
||||
internal class CallHook<T> : IDalamudHook where T : Delegate
|
||||
{
|
||||
private readonly Reloaded.Hooks.AsmHook asmHook;
|
||||
|
||||
private T? detour;
|
||||
private bool activated;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CallHook{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the instruction to replace.</param>
|
||||
/// <param name="detour">Delegate to invoke.</param>
|
||||
internal CallHook(nint address, T detour)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(detour);
|
||||
|
||||
this.detour = detour;
|
||||
this.Address = address;
|
||||
|
||||
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
|
||||
var code = new[]
|
||||
{
|
||||
"use64",
|
||||
$"mov rax, 0x{detourPtr:X8}",
|
||||
"call rax",
|
||||
};
|
||||
|
||||
var opt = new AsmHookOptions
|
||||
{
|
||||
PreferRelativeJump = true,
|
||||
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
|
||||
MaxOpcodeSize = 5,
|
||||
};
|
||||
|
||||
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the hook is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled => this.asmHook.IsEnabled;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Address { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string BackendName => "Reloaded AsmHook";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsDisposed => this.detour == null;
|
||||
|
||||
/// <summary>
|
||||
/// Starts intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (!this.activated)
|
||||
{
|
||||
this.activated = true;
|
||||
this.asmHook.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.asmHook.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a hook from the current process.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
this.detour = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ public abstract class Easing
|
|||
/// Gets the current value of the animation, following unclamped logic.
|
||||
/// </summary>
|
||||
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
|
||||
[Api13ToDo("Map this field to ValueClamped, probably.")]
|
||||
[Api14ToDo("Map this field to ValueClamped, probably.")]
|
||||
public double Value => this.ValueUnclamped;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName
|
|||
var result = new Dictionary<string, string>((int)count);
|
||||
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);
|
||||
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
|
||||
fn->GetString(i, buf, maxStrLen).ThrowOnError();
|
||||
var value = new string(buf);
|
||||
result[key.ToLowerInvariant()] = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId
|
|||
|
||||
var familyIndex = 0u;
|
||||
BOOL exists = false;
|
||||
fixed (void* pName = this.EnglishName)
|
||||
sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
|
||||
fixed (char* pName = this.EnglishName)
|
||||
sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError();
|
||||
if (!exists)
|
||||
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");
|
||||
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId
|
|||
|
||||
var familyIndex = 0u;
|
||||
BOOL exists = false;
|
||||
fixed (void* name = this.Family.EnglishName)
|
||||
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
|
||||
fixed (char* name = this.Family.EnglishName)
|
||||
sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError();
|
||||
if (!exists)
|
||||
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();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,19 +104,19 @@ internal static unsafe class ReShadePeeler
|
|||
fixed (byte* pfn5 = "glBegin"u8)
|
||||
fixed (byte* pfn6 = "vkCreateDevice"u8)
|
||||
{
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == null)
|
||||
continue;
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == null)
|
||||
continue;
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == null)
|
||||
continue;
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == null)
|
||||
continue;
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == null)
|
||||
continue;
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == null)
|
||||
continue;
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0)
|
||||
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
|
|||
|
||||
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
|
||||
{
|
||||
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW);
|
||||
exStyle =
|
||||
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
|
||||
style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
|
||||
exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
|
||||
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
|
||||
if (flags.HasFlag(ImGuiViewportFlags.TopMost))
|
||||
if ((flags & ImGuiViewportFlags.TopMost) != 0)
|
||||
exStyle |= WS.WS_EX_TOPMOST;
|
||||
if ((flags & ImGuiViewportFlags.NoInputs) != 0)
|
||||
exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ using System.Runtime.InteropServices;
|
|||
using System.Text;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Console;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -34,11 +36,14 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
private readonly HCURSOR[] cursors;
|
||||
|
||||
private readonly WndProcDelegate wndProcDelegate;
|
||||
private readonly bool[] imguiMouseIsDown;
|
||||
private readonly nint platformNamePtr;
|
||||
|
||||
private readonly IConsoleVariable<bool> cvLogMouseEvents;
|
||||
|
||||
private ViewportHandler viewportHandler;
|
||||
|
||||
private int mouseButtonsDown;
|
||||
private bool mouseTracked;
|
||||
private long lastTime;
|
||||
|
||||
private nint iniPathPtr;
|
||||
|
|
@ -64,7 +69,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
|
||||
ImGuiBackendFlags.HasSetMousePos |
|
||||
ImGuiBackendFlags.RendererHasViewports |
|
||||
ImGuiBackendFlags.PlatformHasViewports;
|
||||
ImGuiBackendFlags.PlatformHasViewports |
|
||||
ImGuiBackendFlags.HasMouseHoveredViewport;
|
||||
|
||||
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
|
||||
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
|
||||
|
|
@ -74,8 +80,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
|
||||
this.viewportHandler = new(this);
|
||||
|
||||
this.imguiMouseIsDown = new bool[5];
|
||||
|
||||
this.cursors = new HCURSOR[9];
|
||||
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
|
||||
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
|
||||
|
|
@ -86,6 +90,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
this.cursors[(int)ImGuiMouseCursor.ResizeNwse] = LoadCursorW(default, IDC.IDC_SIZENWSE);
|
||||
this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND);
|
||||
this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO);
|
||||
|
||||
this.cvLogMouseEvents = Service<ConsoleManager>.Get().AddVariable(
|
||||
"imgui.log_mouse_events",
|
||||
"Log mouse events to console for debugging",
|
||||
false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -95,8 +104,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
|
||||
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/>
|
||||
public bool UpdateCursor { get; set; } = true;
|
||||
|
||||
|
|
@ -155,6 +162,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
public void NewFrame(int targetWidth, int targetHeight)
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
var focusedWindow = GetForegroundWindow();
|
||||
|
||||
io.DisplaySize.X = targetWidth;
|
||||
io.DisplaySize.Y = targetHeight;
|
||||
|
|
@ -168,9 +176,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
|
||||
this.viewportHandler.UpdateMonitors();
|
||||
|
||||
this.UpdateMousePos();
|
||||
this.UpdateMouseData(focusedWindow);
|
||||
|
||||
this.ProcessKeyEventsWorkarounds();
|
||||
this.ProcessKeyEventsWorkarounds(focusedWindow);
|
||||
|
||||
// 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
|
||||
|
|
@ -224,6 +232,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
|
||||
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_LBUTTONDBLCLK:
|
||||
case WM.WM_RBUTTONDOWN:
|
||||
|
|
@ -233,14 +275,25 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_XBUTTONDOWN:
|
||||
case WM.WM_XBUTTONDBLCLK:
|
||||
{
|
||||
if (this.cvLogMouseEvents.Value)
|
||||
{
|
||||
Log.Verbose(
|
||||
"Handle MouseDown {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}",
|
||||
GetButton(msg, wParam),
|
||||
io.WantCaptureMouse,
|
||||
this.mouseButtonsDown);
|
||||
}
|
||||
|
||||
var button = GetButton(msg, wParam);
|
||||
if (io.WantCaptureMouse)
|
||||
{
|
||||
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero)
|
||||
if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
|
||||
{
|
||||
SetCapture(hWndCurrent);
|
||||
}
|
||||
|
||||
io.MouseDown[button] = true;
|
||||
this.imguiMouseIsDown[button] = true;
|
||||
this.mouseButtonsDown |= 1 << button;
|
||||
io.AddMouseButtonEvent(button, true);
|
||||
return default(LRESULT);
|
||||
}
|
||||
|
||||
|
|
@ -255,14 +308,29 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_MBUTTONUP:
|
||||
case WM.WM_XBUTTONUP:
|
||||
{
|
||||
var button = GetButton(msg, wParam);
|
||||
if (io.WantCaptureMouse && this.imguiMouseIsDown[button])
|
||||
if (this.cvLogMouseEvents.Value)
|
||||
{
|
||||
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
|
||||
ReleaseCapture();
|
||||
Log.Verbose(
|
||||
"Handle MouseUp {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}",
|
||||
GetButton(msg, wParam),
|
||||
io.WantCaptureMouse,
|
||||
this.mouseButtonsDown);
|
||||
}
|
||||
|
||||
io.MouseDown[button] = false;
|
||||
this.imguiMouseIsDown[button] = false;
|
||||
var button = GetButton(msg, wParam);
|
||||
|
||||
// Need to check if we captured the button event away from the game here, otherwise the game might get
|
||||
// a down event but no up event, causing the cursor to get stuck.
|
||||
// Can happen if WantCaptureMouse becomes true in between down and up
|
||||
if (io.WantCaptureMouse && (this.mouseButtonsDown & (1 << button)) != 0)
|
||||
{
|
||||
this.mouseButtonsDown &= ~(1 << button);
|
||||
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
|
||||
{
|
||||
ReleaseCapture();
|
||||
}
|
||||
|
||||
io.AddMouseButtonEvent(button, false);
|
||||
return default(LRESULT);
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +340,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_MOUSEWHEEL:
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +348,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_MOUSEHWHEEL:
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -374,66 +442,89 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
this.viewportHandler.UpdateMonitors();
|
||||
break;
|
||||
|
||||
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
|
||||
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
|
||||
ReleaseCapture();
|
||||
case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
|
||||
io.AddFocusEvent(true);
|
||||
break;
|
||||
|
||||
ImGui.GetIO().WantCaptureMouse = false;
|
||||
ImGui.ClearWindowFocus();
|
||||
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
|
||||
io.AddFocusEvent(false);
|
||||
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
|
||||
// ReleaseCapture();
|
||||
//
|
||||
// ImGui.GetIO().WantCaptureMouse = false;
|
||||
// ImGui.ClearWindowFocus();
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UpdateMousePos()
|
||||
private void UpdateMouseData(HWND focusedWindow)
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
var pt = default(POINT);
|
||||
|
||||
// Depending on if Viewports are enabled, we have to change how we process
|
||||
// the cursor position. If viewports are enabled, we pass the absolute cursor
|
||||
// position to ImGui. Otherwise, we use the old method of passing client-local
|
||||
// mouse position to ImGui.
|
||||
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
|
||||
var mouseScreenPos = default(POINT);
|
||||
var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
|
||||
|
||||
var isAppFocused =
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else
|
||||
// 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.)
|
||||
// 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.)
|
||||
var mousePos = mouseScreenPos;
|
||||
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
|
||||
{
|
||||
io.MousePos.X = float.MinValue;
|
||||
io.MousePos.Y = float.MinValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (io.WantSetMousePos)
|
||||
{
|
||||
pt.x = (int)io.MousePos.X;
|
||||
pt.y = (int)io.MousePos.Y;
|
||||
ClientToScreen(this.hWnd, &pt);
|
||||
SetCursorPos(pt.x, pt.y);
|
||||
// Use game window, otherwise, positions are calculated based on the focused window which might not be the game.
|
||||
// Leads to offsets.
|
||||
ClientToScreen(this.hWnd, &mousePos);
|
||||
}
|
||||
|
||||
if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
|
||||
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)
|
||||
{
|
||||
io.MousePos.X = pt.x;
|
||||
io.MousePos.Y = pt.y;
|
||||
var viewport = this.ViewportFromPoint(mouseScreenPos);
|
||||
io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
io.MousePos.X = float.MinValue;
|
||||
io.MousePos.Y = float.MinValue;
|
||||
io.AddMouseViewportEvent(0);
|
||||
}
|
||||
}
|
||||
|
||||
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
|
||||
{
|
||||
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
|
||||
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
|
||||
}
|
||||
|
||||
private bool UpdateMouseCursor()
|
||||
|
|
@ -451,7 +542,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
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.
|
||||
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
|
||||
|
|
@ -480,7 +571,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
{
|
||||
// See: https://github.com/goatcorp/ImGuiScene/pull/13
|
||||
// > 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++)
|
||||
{
|
||||
// Skip raising modifier keys if the game is focused.
|
||||
|
|
@ -622,7 +713,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND),
|
||||
lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal
|
||||
.GetFunctionPointerForDelegate(this.input.wndProcDelegate),
|
||||
lpszClassName = (ushort*)windowClassNamePtr,
|
||||
lpszClassName = windowClassNamePtr,
|
||||
};
|
||||
|
||||
if (RegisterClassExW(&wcex) == 0)
|
||||
|
|
@ -646,19 +737,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
return;
|
||||
|
||||
var pio = ImGui.GetPlatformIO();
|
||||
|
||||
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;
|
||||
}
|
||||
ImGui.GetPlatformIO().Handle->Monitors.Free();
|
||||
|
||||
fixed (char* windowClassNamePtr = WindowClassName)
|
||||
{
|
||||
UnregisterClassW(
|
||||
(ushort*)windowClassNamePtr,
|
||||
windowClassNamePtr,
|
||||
(HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module));
|
||||
}
|
||||
|
||||
|
|
@ -693,23 +777,26 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
// 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
|
||||
var pio = ImGui.GetPlatformIO();
|
||||
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS);
|
||||
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());
|
||||
pio.Handle->Monitors.Resize(0);
|
||||
|
||||
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
|
||||
// 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);
|
||||
EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
|
||||
|
||||
var monitorIndex = -1;
|
||||
var enumfn = new MonitorEnumProcDelegate(
|
||||
(hMonitor, _, _, _) =>
|
||||
Log.Information("Monitors set up!");
|
||||
foreach (ref var monitor in pio.Handle->Monitors)
|
||||
{
|
||||
Log.Information(
|
||||
"Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
|
||||
monitor.MainPos,
|
||||
monitor.MainSize,
|
||||
monitor.WorkPos,
|
||||
monitor.WorkSize);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
|
||||
{
|
||||
monitorIndex++;
|
||||
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
|
||||
if (!GetMonitorInfoW(hMonitor, &info))
|
||||
return true;
|
||||
|
|
@ -718,33 +805,21 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
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!");
|
||||
for (var i = 0; i < numMonitors; i++)
|
||||
var imMonitor = new ImGuiPlatformMonitor
|
||||
{
|
||||
var monitor = pio.Handle->Monitors[i];
|
||||
Log.Information(
|
||||
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}",
|
||||
i,
|
||||
monitor.MainPos,
|
||||
monitor.MainSize,
|
||||
monitor.WorkPos,
|
||||
monitor.WorkSize);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -781,8 +856,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
{
|
||||
data->Hwnd = CreateWindowExW(
|
||||
(uint)data->DwExStyle,
|
||||
(ushort*)windowClassNamePtr,
|
||||
(ushort*)windowClassNamePtr,
|
||||
windowClassNamePtr,
|
||||
windowClassNamePtr,
|
||||
(uint)data->DwStyle,
|
||||
rect.left,
|
||||
rect.top,
|
||||
|
|
@ -794,6 +869,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
null);
|
||||
}
|
||||
|
||||
if (data->Hwnd == 0)
|
||||
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
|
||||
|
||||
data->HwndOwned = true;
|
||||
viewport.PlatformRequestResize = false;
|
||||
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;
|
||||
|
|
@ -993,7 +1071,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
{
|
||||
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
|
||||
fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title))
|
||||
SetWindowTextW(data->Hwnd, (ushort*)pwszTitle);
|
||||
SetWindowTextW(data->Hwnd, pwszTitle);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
|||
/// <summary>Color stacks to use while evaluating a SeString.</summary>
|
||||
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>
|
||||
/// <remarks>Touched only from the main thread.</remarks>
|
||||
private readonly List<uint> colorStack = [];
|
||||
|
|
@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet
|
|||
foreach (var row in uiColor)
|
||||
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)
|
||||
{
|
||||
// Contains ABGR.
|
||||
this.colorTypes[row.RowId, 0] = row.Dark;
|
||||
this.colorTypes[row.RowId, 1] = row.Light;
|
||||
this.colorTypes[row.RowId, 2] = row.ClassicFF;
|
||||
this.colorTypes[row.RowId, 3] = row.ClearBlue;
|
||||
this.ColorTypes[row.RowId, 0] = row.Dark;
|
||||
this.ColorTypes[row.RowId, 1] = row.Light;
|
||||
this.ColorTypes[row.RowId, 2] = row.ClassicFF;
|
||||
this.ColorTypes[row.RowId, 3] = row.ClearBlue;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
/// <param name="drawState">Draw state.</param>
|
||||
internal void Initialize(scoped ref SeStringDrawState drawState)
|
||||
|
|
@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet
|
|||
}
|
||||
|
||||
// Opacity component is ignored.
|
||||
var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) &&
|
||||
colorTypeIndex < this.colorTypes.GetLength(0)
|
||||
? this.colorTypes[colorTypeIndex, themeIndex]
|
||||
var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) &&
|
||||
colorTypeIndex < this.ColorTypes.GetLength(0)
|
||||
? this.ColorTypes[colorTypeIndex, themeIndex]
|
||||
: 0u;
|
||||
|
||||
rgbaStack.Add(color | 0xFF000000u);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
|||
|
||||
/// <summary>Draws SeString.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class SeStringRenderer : IInternalDisposableService
|
||||
internal class SeStringRenderer : IServiceType
|
||||
{
|
||||
private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
|
||||
private const int ImGuiWindowDcOffset = 0x118;
|
||||
|
|
@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
|
||||
/// <summary>Parsed text fragments from a SeString.</summary>
|
||||
/// <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>
|
||||
/// <remarks>Touched only from the main thread.</remarks>
|
||||
private readonly SeStringColorStackSet colorStackSet;
|
||||
|
||||
/// <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();
|
||||
private readonly SeStringColorStackSet colorStackSetMainThread;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
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")!;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="text">SeString text macro representation.
|
||||
/// Newline characters will be normalized to newline payloads.</param>
|
||||
|
|
@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
text.ReplaceLineEndings("<br>"),
|
||||
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>
|
||||
/// <param name="text">SeString text macro representation.
|
||||
/// Newline characters will be normalized to newline payloads.</param>
|
||||
|
|
@ -113,28 +143,43 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
|
||||
/// <param name="buttonFlags">Button flags to use on link interaction.</param>
|
||||
/// <returns>Interaction result of the rendered text.</returns>
|
||||
public SeStringDrawResult Draw(
|
||||
public unsafe SeStringDrawResult Draw(
|
||||
ReadOnlySeStringSpan sss,
|
||||
scoped in SeStringDrawParams drawParams = default,
|
||||
ImGuiId imGuiId = default,
|
||||
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.
|
||||
if (!imGuiId.IsEmpty())
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
if (drawParams.TargetDrawList is not null && 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.
|
||||
var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
|
||||
using var cleanup = new DisposeSafety.ScopedFinalizer();
|
||||
|
||||
// Reset and initialize the state.
|
||||
this.fragments.Clear();
|
||||
this.colorStackSet.Initialize(ref state);
|
||||
ImFont* font = null;
|
||||
if (drawParams.Font.HasValue)
|
||||
font = drawParams.Font.Value;
|
||||
|
||||
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.
|
||||
using 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.
|
||||
this.CreateTextFragments(ref state);
|
||||
var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments);
|
||||
var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments);
|
||||
|
||||
// Calculate size.
|
||||
var size = Vector2.Zero;
|
||||
|
|
@ -147,25 +192,18 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
|
||||
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;
|
||||
if (drawParams.TargetDrawList is null)
|
||||
{
|
||||
// Handle cases where ImGui.AlignTextToFramePadding has been called.
|
||||
var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset;
|
||||
if (currLineTextBaseOffset != 0f)
|
||||
{
|
||||
itemSize.Y += 2 * currLineTextBaseOffset;
|
||||
foreach (ref var f in fragmentSpan)
|
||||
f.Offset += new Vector2(0, currLineTextBaseOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw all text fragments.
|
||||
var lastRune = default(Rune);
|
||||
|
|
@ -280,15 +318,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
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>
|
||||
/// <param name="state">Draw state.</param>
|
||||
private void CreateTextFragments(ref SeStringDrawState state)
|
||||
|
|
@ -391,7 +420,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
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.
|
||||
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
|
||||
// is an entity.
|
||||
|
|
@ -401,7 +430,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
xy.X = 0;
|
||||
xy.Y += state.LineHeight;
|
||||
w = 0;
|
||||
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
|
||||
CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true;
|
||||
fragment.Offset = xy;
|
||||
|
||||
// Now that the fragment is given its own line, test if it overflows again.
|
||||
|
|
@ -419,16 +448,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
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.
|
||||
// If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
|
||||
// trailing soft hyphens are not considered.
|
||||
if (this.fragments[^1].EndsWithSoftHyphen)
|
||||
xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth;
|
||||
if (state.Fragments[^1].EndsWithSoftHyphen)
|
||||
xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
@ -439,7 +468,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
w = Math.Max(w, xy.X + fragment.VisibleWidth);
|
||||
xy.X += fragment.AdvanceWidth;
|
||||
prev = fragment.To;
|
||||
this.fragments.Add(fragment);
|
||||
state.Fragments.Add(fragment);
|
||||
|
||||
if (fragment.BreakAfter)
|
||||
{
|
||||
|
|
@ -491,7 +520,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
if (gfdTextureSrv != 0)
|
||||
{
|
||||
state.Draw(
|
||||
new ImTextureID(gfdTextureSrv),
|
||||
new(gfdTextureSrv),
|
||||
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
|
||||
size,
|
||||
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
|
||||
|
|
@ -528,7 +557,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
|
||||
return;
|
||||
|
||||
static nint GetGfdTextureSrv()
|
||||
static unsafe nint GetGfdTextureSrv()
|
||||
{
|
||||
var uim = UIModule.Instance();
|
||||
if (uim is null)
|
||||
|
|
@ -553,7 +582,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
/// <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>
|
||||
/// <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();
|
||||
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
|
||||
|
|
@ -710,38 +739,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
firstDisplayRune ?? default,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -12,7 +12,11 @@ public record struct SeStringDrawParams
|
|||
/// <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
|
||||
/// <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>
|
||||
public ImDrawListPtr? TargetDrawList { get; set; }
|
||||
|
||||
|
|
@ -21,16 +25,20 @@ public record struct SeStringDrawParams
|
|||
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <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>
|
||||
/// <remarks>Must be set when specifying a target draw-list or drawing off the main thread.</remarks>
|
||||
public ImFontPtr? Font { get; set; }
|
||||
|
||||
/// <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>
|
||||
/// <remarks>Must be set when specifying a target draw-list or drawing off the main thread.</remarks>
|
||||
public float? FontSize { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the line height ratio.</summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -6,7 +7,10 @@ using System.Text;
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
|
|
@ -14,51 +18,86 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer;
|
|||
|
||||
/// <summary>Calculated values from <see cref="SeStringDrawParams"/> using ImGui styles.</summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe ref struct SeStringDrawState
|
||||
public unsafe ref struct SeStringDrawState : IDisposable
|
||||
{
|
||||
private static readonly int ChannelCount = Enum.GetValues<SeStringDrawChannel>().Length;
|
||||
|
||||
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>
|
||||
/// <param name="span">Raw SeString byte span.</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="splitter">Instance of ImGui Splitter to use.</param>
|
||||
/// <param name="fragments">Fragments.</param>
|
||||
/// <param name="font">Font to use.</param>
|
||||
internal SeStringDrawState(
|
||||
ReadOnlySpan<byte> span,
|
||||
scoped in SeStringDrawParams ssdp,
|
||||
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.GetEntity = ssdp.GetEntity;
|
||||
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.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
|
||||
this.Font = ssdp.EffectiveFont;
|
||||
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
|
||||
this.FontSizeScale = this.FontSize / this.Font->FontSize;
|
||||
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
|
||||
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;
|
||||
|
||||
this.ScreenOffset = ssdp.ScreenOffset ?? throw new ArgumentException(
|
||||
$"{nameof(ssdp.ScreenOffset)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state. (GetCursorScreenPos?)");
|
||||
this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
|
||||
$"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state.");
|
||||
|
||||
// this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
|
||||
// $"{nameof(ssdp.FontSize)} must be set 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.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
|
||||
this.FontSizeScale = this.FontSize / this.Font.FontSize;
|
||||
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
|
||||
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
|
||||
this.Opacity = ssdp.EffectiveOpacity;
|
||||
this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
|
||||
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
|
||||
this.EdgeColor = ssdp.EdgeColor ?? 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.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
|
||||
this.Bold = ssdp.Bold;
|
||||
this.Italic = ssdp.Italic;
|
||||
this.Edge = ssdp.Edge;
|
||||
this.Shadow = ssdp.Shadow;
|
||||
|
||||
this.ColorStackSet.Initialize(ref this);
|
||||
fragments.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
|
||||
|
|
@ -74,7 +113,7 @@ public unsafe ref struct SeStringDrawState
|
|||
public Vector2 ScreenOffset { get; }
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Font"/>
|
||||
public ImFont* Font { get; }
|
||||
public ImFontPtr Font { get; }
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.FontSize"/>
|
||||
public float FontSize { get; }
|
||||
|
|
@ -135,7 +174,7 @@ public unsafe ref struct SeStringDrawState
|
|||
|
||||
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
|
||||
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>
|
||||
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
|
||||
|
|
@ -143,11 +182,20 @@ public unsafe ref struct SeStringDrawState
|
|||
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
|
||||
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; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.splitter.ClearFreeMemory();
|
||||
|
||||
/// <summary>Sets the current channel in the ImGui draw list splitter.</summary>
|
||||
/// <param name="channelIndex">Channel to switch to.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
|
||||
this.splitter->SetCurrentChannel(this.drawList, (int)channelIndex);
|
||||
public void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
|
||||
this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex);
|
||||
|
||||
/// <summary>Draws a single texture.</summary>
|
||||
/// <param name="igTextureId">ImGui texture ID to draw from.</param>
|
||||
|
|
@ -216,9 +264,9 @@ public unsafe ref struct SeStringDrawState
|
|||
/// <summary>Draws a single glyph using current styling configurations.</summary>
|
||||
/// <param name="g">Glyph to draw.</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(
|
||||
MathF.Round(g.X0 * this.FontSizeScale),
|
||||
MathF.Round(g.Y0 * this.FontSizeScale));
|
||||
|
|
@ -268,14 +316,14 @@ public unsafe ref struct SeStringDrawState
|
|||
/// <param name="offset">Offset of the glyph in pixels w.r.t.
|
||||
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</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)
|
||||
return;
|
||||
|
||||
offset += this.ScreenOffset;
|
||||
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.DrawList.AddLine(
|
||||
|
|
@ -302,9 +350,9 @@ public unsafe ref struct SeStringDrawState
|
|||
internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
|
||||
{
|
||||
var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
|
||||
? this.Font->FindGlyph((ushort)rune.Value)
|
||||
: this.Font->FallbackGlyph;
|
||||
return ref *(ImGuiHelpers.ImFontGlyphReal*)p;
|
||||
? (ImFontGlyphPtr)this.Font.FindGlyph((ushort)rune.Value)
|
||||
: this.Font.FallbackGlyph;
|
||||
return ref *(ImGuiHelpers.ImFontGlyphReal*)p.Handle;
|
||||
}
|
||||
|
||||
/// <summary>Gets the glyph corresponding to the given codepoint.</summary>
|
||||
|
|
@ -337,7 +385,7 @@ public unsafe ref struct SeStringDrawState
|
|||
return 0;
|
||||
|
||||
return MathF.Round(
|
||||
this.Font->GetDistanceAdjustmentForPair(
|
||||
this.Font.GetDistanceAdjustmentForPair(
|
||||
(ushort)left.Value,
|
||||
(ushort)right.Value) * this.FontSizeScale);
|
||||
}
|
||||
|
|
@ -350,15 +398,15 @@ public unsafe ref struct SeStringDrawState
|
|||
switch (payload.MacroCode)
|
||||
{
|
||||
case MacroCode.Color:
|
||||
this.colorStackSet.HandleColorPayload(ref this, payload);
|
||||
this.ColorStackSet.HandleColorPayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.EdgeColor:
|
||||
this.colorStackSet.HandleEdgeColorPayload(ref this, payload);
|
||||
this.ColorStackSet.HandleEdgeColorPayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.ShadowColor:
|
||||
this.colorStackSet.HandleShadowColorPayload(ref this, payload);
|
||||
this.ColorStackSet.HandleShadowColorPayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
|
||||
|
|
@ -379,11 +427,11 @@ public unsafe ref struct SeStringDrawState
|
|||
return true;
|
||||
|
||||
case MacroCode.ColorType:
|
||||
this.colorStackSet.HandleColorTypePayload(ref this, payload);
|
||||
this.ColorStackSet.HandleColorTypePayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.EdgeColorType:
|
||||
this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload);
|
||||
this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
|
@ -393,10 +441,9 @@ public unsafe ref struct SeStringDrawState
|
|||
|
||||
/// <summary>Splits the draw list.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal readonly void SplitDrawList() =>
|
||||
this.splitter->Split(this.drawList, ChannelCount);
|
||||
internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount);
|
||||
|
||||
/// <summary>Merges the draw list.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList);
|
||||
internal void MergeDrawList() => this.splitter.Merge(this.drawList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,12 +305,12 @@ internal class DalamudCommands : IServiceType
|
|||
|
||||
chatGui.Print(new SeStringBuilder()
|
||||
.AddItalics("Dalamud:")
|
||||
.AddText($" {Util.GetScmVersion()}")
|
||||
.AddText($" {Versioning.GetScmVersion()}")
|
||||
.Build());
|
||||
|
||||
chatGui.Print(new SeStringBuilder()
|
||||
.AddItalics("FFXIVCS:")
|
||||
.AddText($" {Util.GetGitHashClientStructs()}")
|
||||
.AddText($" {Versioning.GetGitHashClientStructs()}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
() => Service<DalamudInterface>.GetNullable()?.ToggleDevMenu(),
|
||||
VirtualKey.SHIFT);
|
||||
|
||||
if (!configuration.DalamudBetaKind.IsNullOrEmpty())
|
||||
if (Versioning.GetActiveTrack() != "release")
|
||||
{
|
||||
titleScreenMenu.AddEntryCore(
|
||||
Loc.Localize("TSMDalamudDevMenu", "Developer Menu"),
|
||||
|
|
@ -533,6 +533,13 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
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()
|
||||
{
|
||||
this.FrameCount++;
|
||||
|
|
@ -660,6 +667,10 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
{
|
||||
if (this.isImGuiDrawDevMenu)
|
||||
{
|
||||
using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f));
|
||||
barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero);
|
||||
barColor.Push(ImGuiCol.Border, Vector4.Zero);
|
||||
barColor.Push(ImGuiCol.BorderShadow, Vector4.Zero);
|
||||
if (ImGui.BeginMainMenuBar())
|
||||
{
|
||||
var pluginManager = Service<PluginManager>.Get();
|
||||
|
|
@ -832,6 +843,11 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Raise external event through boot"))
|
||||
{
|
||||
ErrorHandling.CrashWithContext("Tést");
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
|
|
@ -849,7 +865,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
}
|
||||
|
||||
ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false, false);
|
||||
ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false);
|
||||
ImGui.MenuItem($"D: {Versioning.GetScmVersion()} CS: {Versioning.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false);
|
||||
ImGui.MenuItem($"CLR: {Environment.Version}", false, false);
|
||||
|
||||
ImGui.EndMenu();
|
||||
|
|
@ -1060,8 +1076,8 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
{
|
||||
ImGui.PushFont(InterfaceManager.MonoFont);
|
||||
|
||||
ImGui.BeginMenu($"{Util.GetActiveTrack() ?? "???"} on {Util.GetGitBranch() ?? "???"}", false);
|
||||
ImGui.BeginMenu($"{Util.GetScmVersion()}", false);
|
||||
ImGui.BeginMenu($"{Versioning.GetActiveTrack() ?? "???"} on {Versioning.GetGitBranch() ?? "???"}", false);
|
||||
ImGui.BeginMenu($"{Versioning.GetScmVersion()}", false);
|
||||
ImGui.BeginMenu(this.FrameCount.ToString("000000"), false);
|
||||
ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false);
|
||||
ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue