Compare commits

..

No commits in common. "master" and "13.0.0.12" have entirely different histories.

498 changed files with 7561 additions and 10664 deletions

View file

@ -8,8 +8,7 @@ import re
import sys
import json
import argparse
import os
from typing import List, Tuple, Optional, Dict, Any
from typing import List, Tuple, Optional
def run_git_command(args: List[str]) -> str:
@ -56,132 +55,46 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
return None
def get_repo_info() -> Tuple[str, str]:
"""Get repository owner and name from git remote."""
try:
remote_url = run_git_command(["config", "--get", "remote.origin.url"])
# Handle both HTTPS and SSH URLs
# SSH: git@github.com:owner/repo.git
# HTTPS: https://github.com/owner/repo.git
match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url)
if match:
owner = match.group(1)
repo = match.group(2)
return owner, repo
else:
print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr)
sys.exit(1)
except:
print("Error: Could not get git remote URL", file=sys.stderr)
sys.exit(1)
def get_commits_between_tags(tag1: str, tag2: str) -> List[str]:
"""Get commit SHAs between two tags."""
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]:
"""Get commits between two tags. Returns list of (message, author) tuples."""
log_output = run_git_command([
"log",
f"{tag2}..{tag1}",
"--format=%H"
"--format=%s|%an|%h"
])
commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
commits = []
for line in log_output.split("\n"):
if "|" in line:
message, author, sha = line.split("|", 2)
commits.append((message.strip(), author.strip(), sha.strip()))
return commits
def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
"""Get PR information for a commit using GitHub API."""
try:
import requests
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
}
if token:
headers["Authorization"] = f"Bearer {token}"
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
prs = response.json()
if prs and len(prs) > 0:
# Return the first PR (most relevant one)
pr = prs[0]
return {
"number": pr["number"],
"title": pr["title"],
"author": pr["user"]["login"],
"url": pr["html_url"]
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
# Commit might not be associated with a PR
return None
elif e.response.status_code == 403:
print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr)
return None
else:
print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
return None
except Exception as e:
print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
return None
return None
def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]:
"""Get PRs between two tags using GitHub API."""
commits = get_commits_between_tags(tag1, tag2)
print(f"Found {len(commits)} commits, fetching PR information...")
prs = []
seen_pr_numbers = set()
for i, commit_sha in enumerate(commits, 1):
if i % 10 == 0:
print(f"Progress: {i}/{len(commits)} commits processed...")
pr_info = get_pr_for_commit(commit_sha, owner, repo, token)
if pr_info and pr_info["number"] not in seen_pr_numbers:
seen_pr_numbers.add(pr_info["number"])
prs.append(pr_info)
return prs
def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]:
"""Filter out PRs matching any of the ignore patterns."""
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]:
"""Filter out commits matching any of the ignore patterns."""
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
filtered = []
for pr in prs:
if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
filtered.append(pr)
for message, author, sha in commits:
if not any(pattern.search(message) for pattern in compiled_patterns):
filtered.append((message, author, sha))
return filtered
def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str],
owner: str, repo: str) -> str:
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str:
"""Generate markdown changelog."""
# Calculate statistics
pr_count = len(prs)
unique_authors = len(set(pr["author"] for pr in prs))
commit_count = len(commits)
unique_authors = len(set(author for _, author, _ in commits))
changelog = f"# Dalamud Release v{version}\n\n"
changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. "
changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
changelog += f"[Click here](<https://github.com/{owner}/{repo}/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n"
changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
changelog += f"[Click here](<https://github.com/goatcorp/Dalamud/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"
@ -191,8 +104,8 @@ def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]
changelog += "## Dalamud Changes\n\n"
for pr in prs:
changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
for message, author, sha in commits:
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n"
return changelog
@ -245,16 +158,11 @@ def main():
required=True,
help="Discord webhook URL"
)
parser.add_argument(
"--github-token",
default=os.environ.get("GITHUB_TOKEN"),
help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit."
)
parser.add_argument(
"--ignore",
action="append",
default=[],
help="Regex patterns to ignore PRs (can be specified multiple times)"
help="Regex patterns to ignore commits (can be specified multiple times)"
)
parser.add_argument(
"--submodule-path",
@ -264,10 +172,6 @@ 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}")
@ -281,18 +185,17 @@ def main():
if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
# Get PRs between tags
prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
prs.reverse()
print(f"Found {len(prs)} PRs")
# Get commits between tags
commits = get_commits_between_tags(latest_tag, previous_tag)
print(f"Found {len(commits)} commits")
# Filter PRs
filtered_prs = filter_prs(prs, args.ignore)
print(f"After filtering: {len(filtered_prs)} PRs")
# Filter commits
filtered_commits = filter_commits(commits, args.ignore)
print(f"After filtering: {len(filtered_commits)} commits")
# Generate changelog
changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
cs_commit_new, cs_commit_old, owner, repo)
changelog = generate_changelog(latest_tag, previous_tag, filtered_commits,
cs_commit_new, cs_commit_old)
print("\n" + "="*50)
print("Generated Changelog:")

View file

@ -6,8 +6,6 @@ on:
tags:
- '*'
permissions: read-all
jobs:
generate-changelog:
runs-on: ubuntu-latest
@ -30,14 +28,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 "Update ClientStructs" \
--ignore "^build:"
--ignore "^Merge" \
--ignore "^build:" \
--ignore "^docs:"
env:
GIT_TERMINAL_PROMPT: 0
- name: Upload changelog as artifact
if: always()

View file

@ -3,7 +3,7 @@ on: [push, pull_request, workflow_dispatch]
concurrency:
group: build_dalamud_${{ github.ref_name }}
cancel-in-progress: false
cancel-in-progress: true
jobs:
build:
@ -23,7 +23,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.0.2
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '10.0.100'
dotnet-version: '9.0.200'
- name: Define VERSION
run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)

View file

@ -1,8 +1,8 @@
name: Rollup changes to next version
on:
# push:
# branches:
# - master
push:
branches:
- master
workflow_dispatch:
jobs:

View file

@ -1,8 +1,30 @@
{
"$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",
@ -21,48 +43,9 @@
"VSCode"
]
},
"ExecutableTarget": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"Restore",
"SetCILogging",
"Test"
]
},
"Verbosity": {
"type": "string",
"description": "",
"enum": [
"Verbose",
"Normal",
"Minimal",
"Quiet"
]
},
"NukeBuild": {
"properties": {
"Continue": {
"IsDocsBuild": {
"type": "boolean",
"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"
"description": "Whether we are building for documentation - emits generated files"
},
"NoLogo": {
"type": "boolean",
@ -91,46 +74,65 @@
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"$ref": "#/definitions/ExecutableTarget"
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
}
},
"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": {
"$ref": "#/definitions/ExecutableTarget"
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
}
},
"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": [
"Debug",
"Release"
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
}
}
},
{
"$ref": "#/definitions/NukeBuild"
}
]
}
}

View file

@ -108,11 +108,6 @@ 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);

View file

@ -44,7 +44,6 @@ struct DalamudStartInfo {
std::string ConfigurationPath;
std::string LogPath;
std::string LogName;
std::string TempDirectory;
std::string PluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;

View file

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

View file

@ -331,51 +331,6 @@ 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))

View file

@ -31,8 +31,6 @@ 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;
@ -124,7 +122,6 @@ 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
@ -193,11 +190,7 @@ 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 (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
{
stackTrace = std::wstring(g_external_event_info);
}
else if (!g_clr)
if (!g_clr)
{
stackTrace = L"(no CLR stack trace available)";
}
@ -258,12 +251,6 @@ 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
@ -447,16 +434,3 @@ 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);
}

View file

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

View file

@ -34,12 +34,6 @@ 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>

View file

@ -0,0 +1,111 @@
<?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>

View file

@ -0,0 +1,67 @@
<?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>

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

View file

@ -0,0 +1,48 @@
#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;
}

View file

@ -0,0 +1 @@
#pragma once

View file

@ -0,0 +1 @@
MAINICON ICON "dalamud.ico"

View file

@ -13,13 +13,12 @@
</PropertyGroup>
<PropertyGroup Label="Output">
<OutputType>Exe</OutputType>
<OutputType>Library</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">

View file

@ -25,20 +25,34 @@ namespace Dalamud.Injector
/// <summary>
/// Entrypoint to the program.
/// </summary>
public sealed class Program
public sealed class EntryPoint
{
/// <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="argsArray">Command line arguments.</param>
/// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">byte** string arguments.</param>
/// <returns>Return value (HRESULT).</returns>
public static int Main(string[] argsArray)
public static int Main(int argc, IntPtr argvPtr)
{
try
{
// API14 TODO: Refactor
var args = argsArray.ToList();
args.Insert(0, Assembly.GetExecutingAssembly().Location);
List<string> args = new(argc);
unsafe
{
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args);
args.Remove("-v"); // Remove "verbose" flag
@ -291,7 +305,6 @@ 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;
@ -322,10 +335,6 @@ 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..]);
@ -438,7 +447,6 @@ namespace Dalamud.Injector
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.TempDirectory = tempDirectory;
startInfo.Language = clientLanguage;
startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs;

View file

@ -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,6 +7,8 @@ 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
@ -25,6 +27,8 @@ 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}"
@ -45,6 +49,8 @@ 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}"
@ -75,17 +81,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}"
ProjectSection(SolutionItems) = preProject
generators\Directory.Build.props = generators\Directory.Build.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -108,6 +103,10 @@ 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
@ -184,23 +183,13 @@ Global
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
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}
@ -220,9 +209,6 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
{8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
@ -19,12 +20,9 @@ using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Storage;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud.Configuration.Internal;
@ -93,7 +91,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a dictionary of seen FTUE levels.
/// </summary>
public Dictionary<string, int> SeenFtueLevels { get; set; } = [];
public Dictionary<string, int> SeenFtueLevels { get; set; } = new();
/// <summary>
/// Gets or sets the last loaded Dalamud version.
@ -113,7 +111,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list of custom repos.
/// </summary>
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = [];
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
/// <summary>
/// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed.
@ -123,12 +121,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list of hidden plugins.
/// </summary>
public List<string> HiddenPluginInternalName { get; set; } = [];
public List<string> HiddenPluginInternalName { get; set; } = new();
/// <summary>
/// Gets or sets a list of seen plugins.
/// </summary>
public List<string> SeenPluginInternalName { get; set; } = [];
public List<string> SeenPluginInternalName { get; set; } = new();
/// <summary>
/// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
@ -136,14 +134,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// However by specifiying this value manually, you can add arbitrary files outside the normal
/// file paths.
/// </summary>
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = [];
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = new();
/// <summary>
/// Gets or sets a list of additional locations that dev plugins should be loaded from. This can
/// be either a DLL or folder, but should be the absolute path, or a path relative to the currently
/// injected Dalamud instance.
/// </summary>
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = [];
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = new();
/// <summary>
/// Gets or sets the global UI scale.
@ -225,7 +223,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list representing the command history for the Dalamud Console.
/// </summary>
public List<string> LogCommandHistory { get; set; } = [];
public List<string> LogCommandHistory { get; set; } = new();
/// <summary>
/// Gets or sets a value indicating whether the dev bar should open at startup.
@ -497,16 +495,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
#pragma warning restore SA1516
#pragma warning restore SA1600
/// <summary>
/// Gets or sets a list of badge passwords used to unlock badges.
/// </summary>
public List<string> UsedBadgePasswords { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether badges should be shown on the title screen.
/// </summary>
public bool ShowBadgesOnTitleScreen { get; set; } = true;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
@ -601,7 +589,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
var winAnimEnabled = 0;
bool success;
var success = false;
unsafe
{
success = Windows.Win32.PInvoke.SystemParametersInfo(

View file

@ -31,5 +31,5 @@ internal sealed class DevPluginSettings
/// <summary>
/// Gets or sets a list of validation problems that have been dismissed by the user.
/// </summary>
public List<string> DismissedValidationProblems { get; set; } = [];
public List<string> DismissedValidationProblems { get; set; } = new();
}

View file

@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </summary>
[Api15ToDo("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.")]
[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.")]
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
@ -17,9 +17,9 @@ namespace Dalamud.Console;
[ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")]
internal partial class ConsoleManager : IServiceType
{
private static readonly ModuleLog Log = ModuleLog.Create<ConsoleManager>();
private static readonly ModuleLog Log = new("CON");
private Dictionary<string, IConsoleEntry> entries = [];
private Dictionary<string, IConsoleEntry> entries = new();
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleManager"/> class.
@ -99,7 +99,10 @@ internal partial class ConsoleManager : IServiceType
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(alias);
var target = this.FindEntry(name) ?? throw new EntryNotFoundException(name);
var target = this.FindEntry(name);
if (target == null)
throw new EntryNotFoundException(name);
if (this.FindEntry(alias) != null)
throw new InvalidOperationException($"Entry '{alias}' already exists.");
@ -343,7 +346,7 @@ internal partial class ConsoleManager : IServiceType
private static class Traits
{
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
{
if (argument == null && !typeof(T).IsValueType)
throw new ArgumentNullException(paramName);

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@ -65,7 +65,7 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
private readonly List<IConsoleEntry> trackedEntries = [];
private readonly List<IConsoleEntry> trackedEntries = new();
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class.

View file

@ -9,14 +9,11 @@ using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Hooking.Internal.Verification;
using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
@ -76,11 +73,6 @@ internal sealed unsafe class Dalamud : IServiceType
scanner,
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
using (Timings.Start("HookVerifier Init"))
{
HookVerifier.Initialize(scanner);
}
// Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir);

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>14.0.2.2</DalamudVersion>
<DalamudVersion>13.0.0.12</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -65,6 +65,7 @@
<PackageReference Include="CheapLoc" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" />
<PackageReference Include="goatcorp.Reloaded.Assembler" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Lumina" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
@ -72,6 +73,8 @@
<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" />
@ -82,20 +85,14 @@
<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>
<ItemGroup>
<ProjectReference Include="..\generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<None Remove="EnumCloneMap.txt"/>
<AdditionalFiles Include="EnumCloneMap.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>
@ -125,8 +122,6 @@
<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>
@ -231,4 +226,9 @@
<!-- 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>

View file

@ -124,13 +124,6 @@ public enum DalamudAsset
[DalamudAssetPath("UIRes", "tsmShade.png")]
TitleScreenMenuShade = 1013,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: Atlas containing badges.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "badgeAtlas.png")]
BadgeAtlas = 1015,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
/// </summary>
@ -158,7 +151,7 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")]
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
FontAwesomeFreeSolid = 2003,
/// <summary>

View file

@ -8,13 +8,11 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Lumina;
using Lumina.Data;
using Lumina.Excel;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Data;
@ -84,13 +82,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo =
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
dalamud.StartInfo.TroubleshootingPackData);
// 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;
this.HasModifiedGameDataFiles =
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);

View file

@ -3,9 +3,7 @@ using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using Lumina.Text.ReadOnly;
namespace Dalamud.Data;
@ -15,7 +13,7 @@ namespace Dalamud.Data;
/// </summary>
internal sealed unsafe class RsvResolver : IDisposable
{
private static readonly ModuleLog Log = ModuleLog.Create<RsvResolver>();
private static readonly ModuleLog Log = new("RsvProvider");
private readonly Hook<LayoutWorld.Delegates.AddRsvString> addRsvStringHook;

View file

@ -1,6 +1,8 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -14,13 +16,10 @@ using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Support;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@ -193,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}]",
Versioning.GetScmVersion(),
Versioning.GetGitHashClientStructs(),
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload();
@ -264,7 +263,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}";
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess();
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
// Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess);
@ -293,6 +292,7 @@ public sealed class EntryPoint
}
var pluginInfo = string.Empty;
var supportText = ", please visit us on Discord for more help";
try
{
var pm = Service<PluginManager>.GetNullable();
@ -300,6 +300,9 @@ public sealed class EntryPoint
if (plugin != null)
{
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
if (plugin.IsThirdParty)
supportText = string.Empty;
}
}
catch
@ -307,18 +310,31 @@ public sealed class EntryPoint
// ignored
}
Log.CloseAndFlush();
const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL;
var result = Windows.Win32.PInvoke.MessageBox(
new HWND(Process.GetCurrentProcess().MainWindowHandle),
$"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?",
"Dalamud",
flags);
ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
if (result == MESSAGEBOX_RESULT.IDYES)
{
Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.ForceSave();
}
Log.CloseAndFlush();
Environment.Exit(-1);
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush();
Environment.Exit(-1);
break;
}
Environment.Exit(-1);
}
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)

View file

@ -1,3 +0,0 @@
# Format: Target.Full.TypeName = Source.Full.EnumTypeName
# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum
Dalamud.Game.Agent.AgentId = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId

View file

@ -0,0 +1,107 @@
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;
}
}
}
}

View file

@ -1,4 +1,5 @@
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;

View file

@ -9,6 +9,7 @@ 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;
@ -24,28 +25,32 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// </summary>
public static readonly Guid DalamudInternalKey = Guid.NewGuid();
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
private static readonly ModuleLog Log = new("AddonEventManager");
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly Hook<AtkUnitManager.Delegates.UpdateCursor> onUpdateCursor;
private readonly AddonEventManagerAddressResolver address;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
private AtkCursor.CursorType? cursorOverride;
private AddonCursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
private AddonEventManager()
private AddonEventManager(TargetSigScanner sigScanner)
{
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<AtkUnitManager.Delegates.UpdateCursor>.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour);
this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
@ -53,6 +58,8 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.onUpdateCursor.Enable();
}
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -110,7 +117,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 = (AtkCursor.CursorType)cursor;
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
/// <summary>
/// Un-forces the game cursor.
@ -161,7 +168,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
}
}
private void UpdateCursorDetour(AtkUnitManager* thisPtr)
private nint UpdateCursorDetour(RaptureAtkModule* module)
{
try
{
@ -169,14 +176,13 @@ internal unsafe class AddonEventManager : IInternalDisposableService
if (this.cursorOverride is not null && atkStage is not null)
{
ref var atkCursor = ref atkStage->AtkCursor;
if (atkCursor.Type != this.cursorOverride)
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
if (cursor != this.cursorOverride)
{
atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
return;
return nint.Zero;
}
}
catch (Exception e)
@ -184,7 +190,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Error(e, "Exception in UpdateCursorDetour.");
}
this.onUpdateCursor!.Original(thisPtr);
return this.onUpdateCursor!.Original(module);
}
}

View file

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

View file

@ -61,11 +61,6 @@ public enum AddonEventType : byte
/// </summary>
InputBaseInputReceived = 15,
/// <summary>
/// Fired at the very beginning of AtkInputManager.HandleInput on AtkStage.ViewportEventManager. Used in LovmMiniMap.
/// </summary>
RawInputData = 16,
/// <summary>
/// Focus Start.
/// </summary>
@ -112,12 +107,7 @@ public enum AddonEventType : byte
SliderReleased = 30,
/// <summary>
/// AtkComponentList Button Press.
/// </summary>
ListButtonPress = 31,
/// <summary>
/// AtkComponentList Roll Over.
/// AtkComponentList RollOver.
/// </summary>
ListItemRollOver = 33,
@ -136,31 +126,11 @@ public enum AddonEventType : byte
/// </summary>
ListItemDoubleClick = 36,
/// <summary>
/// AtkComponentList Highlight.
/// </summary>
ListItemHighlight = 37,
/// <summary>
/// AtkComponentList Select.
/// </summary>
ListItemSelect = 38,
/// <summary>
/// AtkComponentList Pad Drag Drop Begin.
/// </summary>
ListItemPadDragDropBegin = 40,
/// <summary>
/// AtkComponentList Pad Drag Drop End.
/// </summary>
ListItemPadDragDropEnd = 41,
/// <summary>
/// AtkComponentList Pad Drag Drop Insert.
/// </summary>
ListItemPadDragDropInsert = 42,
/// <summary>
/// AtkComponentDragDrop Begin.
/// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
@ -172,22 +142,12 @@ public enum AddonEventType : byte
/// </summary>
DragDropEnd = 51,
/// <summary>
/// AtkComponentDragDrop Insert Attempt.
/// </summary>
DragDropInsertAttempt = 52,
/// <summary>
/// AtkComponentDragDrop Insert.
/// Sent when dropping an icon into a hotbar/inventory slot or similar.
/// </summary>
DragDropInsert = 53,
/// <summary>
/// AtkComponentDragDrop Can Accept Check.
/// </summary>
DragDropCanAcceptCheck = 54,
/// <summary>
/// AtkComponentDragDrop Roll Over.
/// </summary>
@ -205,18 +165,23 @@ public enum AddonEventType : byte
DragDropDiscard = 57,
/// <summary>
/// AtkComponentDragDrop Click.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// Drag Drop Unknown.
/// </summary>
DragDropClick = 58,
[Obsolete("Use DragDropDiscard", true)]
DragDropUnk54 = 54,
/// <summary>
/// AtkComponentDragDrop Cancel.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// </summary>
[Obsolete("Renamed to DragDropClick")]
DragDropCancel = 58,
/// <summary>
/// Drag Drop Unknown.
/// </summary>
[Obsolete("Use DragDropCancel", true)]
DragDropUnk55 = 55,
/// <summary>
/// AtkComponentIconText Roll Over.
/// </summary>
@ -252,11 +217,6 @@ public enum AddonEventType : byte
/// </summary>
TimerEnd = 65,
/// <summary>
/// AtkTimer Start.
/// </summary>
TimerStart = 66,
/// <summary>
/// AtkSimpleTween Progress.
/// </summary>
@ -287,11 +247,6 @@ public enum AddonEventType : byte
/// </summary>
WindowChangeScale = 72,
/// <summary>
/// AtkTimeline Active Label Changed.
/// </summary>
TimelineActiveLabelChanged = 75,
/// <summary>
/// AtkTextNode Link Mouse Click.
/// </summary>

View file

@ -5,6 +5,7 @@ using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Gui;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -15,7 +16,7 @@ namespace Dalamud.Game.Addon.Events;
/// </summary>
internal unsafe class PluginEventController : IDisposable
{
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
private static readonly ModuleLog Log = new("AddonEventManager");
/// <summary>
/// Initializes a new instance of the <see cref="PluginEventController"/> class.
@ -27,7 +28,7 @@ internal unsafe class PluginEventController : IDisposable
private AddonEventListener EventListener { get; init; }
private List<AddonEventEntry> Events { get; } = [];
private List<AddonEventEntry> Events { get; } = new();
/// <summary>
/// Adds a tracked event.

View file

@ -5,24 +5,19 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Base class for AddonLifecycle AddonArgTypes.
/// </summary>
public class AddonArgs
public abstract unsafe class AddonArgs
{
/// <summary>
/// Constant string representing the name of an addon that is invalid.
/// </summary>
public const string InvalidAddon = "NullAddon";
/// <summary>
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
/// </summary>
internal AddonArgs()
{
}
private string? addonName;
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName { get; private set; } = InvalidAddon;
public string AddonName => this.GetAddonName();
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
@ -30,17 +25,55 @@ public class AddonArgs
public AtkUnitBasePtr Addon
{
get;
internal set
{
field = value;
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
this.AddonName = value.Name;
}
internal set;
}
/// <summary>
/// Gets the type of these args.
/// </summary>
public virtual AddonArgsType Type => AddonArgsType.Generic;
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;
}
}

View file

@ -1,22 +0,0 @@
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; }
}

View file

@ -0,0 +1,24 @@
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();
}

View file

@ -0,0 +1,24 @@
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();
}

View file

@ -1,22 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnFocusChanged events.
/// </summary>
public class AddonFocusChangedArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFocusChangedArgs"/> class.
/// </summary>
internal AddonFocusChangedArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.FocusChanged;
/// <summary>
/// Gets or sets a value indicating whether the window is being focused or unfocused.
/// </summary>
public bool ShouldFocus { get; set; }
}

View file

@ -1,32 +0,0 @@
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; }
}

View file

@ -3,12 +3,13 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonReceiveEventArgs : AddonArgs
public class AddonReceiveEventArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </summary>
internal AddonReceiveEventArgs()
[Obsolete("Not intended for public construction.", false)]
public AddonReceiveEventArgs()
{
}
@ -31,7 +32,23 @@ public class AddonReceiveEventArgs : AddonArgs
public nint AtkEvent { get; set; }
/// <summary>
/// Gets or sets the pointer to an AtkEventData for this event message.
/// Gets or sets the pointer to a block of data for this event message.
/// </summary>
public nint AtkEventData { get; set; }
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;
}
}

View file

@ -1,22 +1,17 @@
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
public class AddonRefreshArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
/// </summary>
internal AddonRefreshArgs()
[Obsolete("Not intended for public construction.", false)]
public AddonRefreshArgs()
{
}
@ -36,32 +31,19 @@ public class AddonRefreshArgs : AddonArgs
/// <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);
/// <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
{
get
{
for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
#pragma warning restore CS0618 // Type or member is obsolete
}
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
yield return ptr;
}
}
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
}
}

View file

@ -3,12 +3,13 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnRequestedUpdate events.
/// </summary>
public class AddonRequestedUpdateArgs : AddonArgs
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </summary>
internal AddonRequestedUpdateArgs()
[Obsolete("Not intended for public construction.", false)]
public AddonRequestedUpdateArgs()
{
}
@ -24,4 +25,18 @@ public class AddonRequestedUpdateArgs : AddonArgs
/// 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;
}
}

View file

@ -1,22 +1,17 @@
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
public class AddonSetupArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary>
internal AddonSetupArgs()
[Obsolete("Not intended for public construction.", false)]
public AddonSetupArgs()
{
}
@ -36,32 +31,19 @@ public class AddonSetupArgs : AddonArgs
/// <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);
/// <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
{
get
{
for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
#pragma warning restore CS0618 // Type or member is obsolete
}
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
yield return ptr;
}
}
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
}
}

View file

@ -1,27 +0,0 @@
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; }
}

View file

@ -0,0 +1,45 @@
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;
}
}

View file

@ -5,16 +5,26 @@
/// </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>
@ -29,24 +39,4 @@ 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,
/// <summary>
/// Contains argument data for OnFocusChanged.
/// </summary>
FocusChanged,
}

View file

@ -29,6 +29,7 @@ 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>
@ -41,6 +42,7 @@ 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>
@ -60,6 +62,7 @@ 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>
@ -115,102 +118,4 @@ 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,
/// <summary>
/// An event that is fired before an addon processes its FocusChanged method.
/// </summary>
PreFocusChanged,
/// <summary>
/// An event that is fired after a addon processes its FocusChanged method.
/// </summary>
PostFocusChanged,
}

View file

@ -1,15 +1,16 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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;
@ -20,56 +21,75 @@ 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 = ModuleLog.Create<AddonLifecycle>();
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private bool isInvokingListeners;
[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;
[ServiceManager.ServiceConstructor]
private AddonLifecycle()
private AddonLifecycle(TargetSigScanner sigScanner)
{
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
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();
}
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> <br/>
/// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
/// </summary>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
this.onAddonSetupHook.Dispose();
this.onAddonFinalizeHook.Dispose();
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
AllocatedTables.ForEach(entry => entry.Dispose());
AllocatedTables.Clear();
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal static AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
foreach (var receiveEventListener in this.ReceiveEventListeners)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
return null;
receiveEventListener.Dispose();
}
return matchedTable.OriginalVirtualTable;
}
/// <summary>
@ -78,15 +98,21 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener)
{
if (this.isInvokingListeners)
this.framework.RunOnTick(() =>
{
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
}
else
this.EventListeners.Add(listener);
// 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 })
{
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
receiveEventListener.TryEnable();
}
}
});
}
/// <summary>
/// Unregisters the listener from events.
@ -94,17 +120,28 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
listener.IsRequestedToClear = true;
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
listener.Removed = true;
if (this.isInvokingListeners)
this.framework.RunOnTick(() =>
{
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
}
else
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 })
{
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
// 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();
}
}
}
});
}
/// <summary>
/// Invoke listeners for the specified event type.
@ -114,17 +151,19 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
this.isInvokingListeners = true;
// 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))
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
foreach (var listener in this.EventListeners)
{
foreach (var listener in globalListeners)
{
if (listener.IsRequestedToClear) continue;
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
{
@ -132,86 +171,206 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
}
}
// Handle listeners that are listening for this addon and event type specifically
if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
foreach (var listener in 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 (listener.IsRequestedToClear) continue;
try
// 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)
{
listener.FunctionDelegate.Invoke(eventType, args);
if (!existingListener.AddonNames.Contains(addonName))
{
existingListener.AddonNames.Add(addonName);
}
catch (Exception e)
}
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
else
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
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();
}
}
}
}
this.isInvokingListeners = false;
}
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);
private void RegisterListenerMethod(AddonLifecycleEventListener listener)
// If there are no more listeners let's remove and dispose.
if (eventListener.AddonNames.Count is 0)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
return;
}
}
// 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.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
{
return;
}
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
private void UnregisterListenerMethod(AddonLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
{
addonListener.Remove(listener);
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
}
}
}
private void OnAddonInitialize(AtkUnitBase* addon)
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
try
{
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));
this.RegisterReceiveEventHook(addon);
}
catch (Exception e)
{
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
}
this.onInitializeAddonHook!.Original(addon);
}
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;
[Conditional("DEBUG")]
private void LogInitialize(string addonName)
try
{
Log.Debug($"Initializing {addonName}");
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)
{
try
{
var addonName = atkUnitBase[0]->NameString;
this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.Clear();
arg.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
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);
}
}
@ -228,7 +387,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = [];
private readonly List<AddonLifecycleEventListener> eventListeners = new();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -305,8 +464,4 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
});
}
}
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
}

View file

@ -0,0 +1,56 @@
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");
}
}

View file

@ -26,6 +26,11 @@ 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>
@ -35,9 +40,4 @@ internal class AddonLifecycleEventListener
/// Gets the delegate this listener invokes.
/// </summary>
public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
/// <summary>
/// Gets or sets if the listener is requested to be cleared.
/// </summary>
internal bool IsRequestedToClear { get; set; }
}

View file

@ -0,0 +1,112 @@
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);
}
}

View file

@ -0,0 +1,80 @@
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;
}
}

View file

@ -1,679 +0,0 @@
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 AddonFocusChangedArgs focusChangedArgs = new();
private readonly AtkUnitBase* atkUnitBase;
// 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;
private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction;
/// <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;
this.onFocusChangeFunction = this.OnAddonFocusChange;
// 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);
this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged<AtkUnitBase*, bool, void>)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction);
}
/// <summary>
/// Gets the original virtual table address for this addon.
/// </summary>
internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this addon.
/// </summary>
internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; }
/// <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.");
}
}
private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused)
{
try
{
this.LogEvent(EnableLogging);
this.focusChangedArgs.Addon = thisPtr;
this.focusChangedArgs.ShouldFocus = isFocused;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs);
isFocused = this.focusChangedArgs.ShouldFocus;
try
{
this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange.");
}
}
[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}");
}
}
}

View file

@ -1,39 +0,0 @@
using Dalamud.Game.NativeWrapper;
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Base class for AgentLifecycle AgentArgTypes.
/// </summary>
public unsafe class AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentArgs"/> class.
/// </summary>
internal AgentArgs()
{
}
/// <summary>
/// Gets the pointer to the Agents AgentInterface*.
/// </summary>
public AgentInterfacePtr Agent { get; internal set; }
/// <summary>
/// Gets the agent id.
/// </summary>
public AgentId AgentId { get; internal set; }
/// <summary>
/// Gets the type of these args.
/// </summary>
public virtual AgentArgsType Type => AgentArgsType.Generic;
/// <summary>
/// Gets the typed pointer to the Agents AgentInterface*.
/// </summary>
/// <typeparam name="T">AgentInterface.</typeparam>
/// <returns>Typed pointer to contained Agents AgentInterface.</returns>
public T* GetAgentPointer<T>() where T : unmanaged
=> (T*)this.Agent.Address;
}

View file

@ -1,22 +0,0 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentClassJobChangeArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentClassJobChangeArgs"/> class.
/// </summary>
internal AgentClassJobChangeArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.ClassJobChange;
/// <summary>
/// Gets or sets a value indicating what the new ClassJob is.
/// </summary>
public byte ClassJobId { get; set; }
}

View file

@ -1,22 +0,0 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentGameEventArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentGameEventArgs"/> class.
/// </summary>
internal AgentGameEventArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.GameEvent;
/// <summary>
/// Gets or sets a value representing which gameEvent was triggered.
/// </summary>
public int GameEvent { get; set; }
}

View file

@ -1,27 +0,0 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentLevelChangeArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentLevelChangeArgs"/> class.
/// </summary>
internal AgentLevelChangeArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.LevelChange;
/// <summary>
/// Gets or sets a value indicating which ClassJob was switched to.
/// </summary>
public byte ClassJobId { get; set; }
/// <summary>
/// Gets or sets a value indicating what the new level is.
/// </summary>
public ushort Level { get; set; }
}

View file

@ -1,37 +0,0 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for ReceiveEvent events.
/// </summary>
public class AgentReceiveEventArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentReceiveEventArgs"/> class.
/// </summary>
internal AgentReceiveEventArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.ReceiveEvent;
/// <summary>
/// Gets or sets the AtkValue return value for this event message.
/// </summary>
public nint ReturnValue { get; set; }
/// <summary>
/// Gets or sets the AtkValue array for this event message.
/// </summary>
public nint AtkValues { get; set; }
/// <summary>
/// Gets or sets the AtkValue count for this event message.
/// </summary>
public uint ValueCount { get; set; }
/// <summary>
/// Gets or sets the event kind for this event message.
/// </summary>
public ulong EventKind { get; set; }
}

View file

@ -1,32 +0,0 @@
namespace Dalamud.Game.Agent;
/// <summary>
/// Enumeration for available AgentLifecycle arg data.
/// </summary>
public enum AgentArgsType
{
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary>
/// Contains argument data for ReceiveEvent.
/// </summary>
ReceiveEvent,
/// <summary>
/// Contains argument data for GameEvent.
/// </summary>
GameEvent,
/// <summary>
/// Contains argument data for LevelChange.
/// </summary>
LevelChange,
/// <summary>
/// Contains argument data for ClassJobChange.
/// </summary>
ClassJobChange,
}

View file

@ -1,87 +0,0 @@
namespace Dalamud.Game.Agent;
/// <summary>
/// Enumeration for available AgentLifecycle events.
/// </summary>
public enum AgentEvent
{
/// <summary>
/// An event that is fired before the agent processes its Receive Event Function.
/// </summary>
PreReceiveEvent,
/// <summary>
/// An event that is fired after the agent has processed its Receive Event Function.
/// </summary>
PostReceiveEvent,
/// <summary>
/// An event that is fired before the agent processes its Filtered Receive Event Function.
/// </summary>
PreReceiveEventWithResult,
/// <summary>
/// An event that is fired after the agent has processed its Filtered Receive Event Function.
/// </summary>
PostReceiveEventWithResult,
/// <summary>
/// An event that is fired before the agent processes its Show Function.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after the agent has processed its Show Function.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before the agent processes its Hide Function.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after the agent has processed its Hide Function.
/// </summary>
PostHide,
/// <summary>
/// An event that is fired before the agent processes its Update Function.
/// </summary>
PreUpdate,
/// <summary>
/// An event that is fired after the agent has processed its Update Function.
/// </summary>
PostUpdate,
/// <summary>
/// An event that is fired before the agent processes its Game Event Function.
/// </summary>
PreGameEvent,
/// <summary>
/// An event that is fired after the agent has processed its Game Event Function.
/// </summary>
PostGameEvent,
/// <summary>
/// An event that is fired before the agent processes its Game Event Function.
/// </summary>
PreLevelChange,
/// <summary>
/// An event that is fired after the agent has processed its Level Change Function.
/// </summary>
PostLevelChange,
/// <summary>
/// An event that is fired before the agent processes its ClassJob Change Function.
/// </summary>
PreClassJobChange,
/// <summary>
/// An event that is fired after the agent has processed its ClassJob Change Function.
/// </summary>
PostClassJobChange,
}

View file

@ -1,344 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Agent.AgentArgTypes;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Agent;
/// <summary>
/// This class provides events for in-game agent lifecycles.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class AgentLifecycle : IInternalDisposableService
{
/// <summary>
/// Gets a list of all allocated agent virtual tables.
/// </summary>
public static readonly List<AgentVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AgentLifecycle");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AgentModule.Delegates.Ctor>? onInitializeAgentsHook;
private bool isInvokingListeners;
[ServiceManager.ServiceConstructor]
private AgentLifecycle()
{
var agentModuleInstance = AgentModule.Instance();
// Hook is only used to determine appropriate timing for replacing Agent Virtual Tables
// If the agent module is already initialized, then we can replace the tables safely.
if (agentModuleInstance is null)
{
this.onInitializeAgentsHook = Hook<AgentModule.Delegates.Ctor>.FromAddress((nint)AgentModule.MemberFunctionPointers.Ctor, this.OnAgentModuleInitialize);
this.onInitializeAgentsHook.Enable();
}
else
{
// For safety because this might be injected async, we will make sure we are on the main thread first.
this.framework.RunOnFrameworkThread(() => this.ReplaceVirtualTables(agentModuleInstance));
}
}
/// <summary>
/// Gets a list of all AgentLifecycle Event Listeners.
/// </summary> <br/>
/// Mapping is: EventType -> ListenerList
internal Dictionary<AgentEvent, Dictionary<AgentId, HashSet<AgentLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onInitializeAgentsHook?.Dispose();
this.onInitializeAgentsHook = null;
AllocatedTables.ForEach(entry => entry.Dispose());
AllocatedTables.Clear();
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal static AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
return null;
}
return matchedTable.OriginalVirtualTable;
}
/// <summary>
/// Register a listener for the target event and agent.
/// </summary>
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AgentLifecycleEventListener listener)
{
if (this.isInvokingListeners)
{
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
}
}
/// <summary>
/// Unregisters the listener from events.
/// </summary>
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AgentLifecycleEventListener listener)
{
listener.IsRequestedToClear = true;
if (this.isInvokingListeners)
{
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
}
}
/// <summary>
/// Invoke listeners for the specified event type.
/// </summary>
/// <param name="eventType">Event Type.</param>
/// <param name="args">AgentARgs.</param>
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AgentEvent eventType, AgentArgs args, [CallerMemberName] string blame = "")
{
this.isInvokingListeners = true;
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return;
// Handle listeners for this event type that don't care which agent is triggering it
if (agentListeners.TryGetValue((AgentId)uint.MaxValue, out var globalListeners))
{
foreach (var listener in globalListeners)
{
if (listener.IsRequestedToClear) continue;
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global agent event listener.");
}
}
}
// Handle listeners that are listening for this agent and event type specifically
if (agentListeners.TryGetValue(args.AgentId, out var agentListener))
{
foreach (var listener in agentListener)
{
if (listener.IsRequestedToClear) continue;
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {args.AgentId}.");
}
}
}
this.isInvokingListeners = false;
}
private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule)
{
this.onInitializeAgentsHook!.Original(thisPtr, uiModule);
try
{
this.ReplaceVirtualTables(thisPtr);
// We don't need this hook anymore, it did its job!
this.onInitializeAgentsHook!.Dispose();
this.onInitializeAgentsHook = null;
}
catch (Exception e)
{
Log.Error(e, "Exception in AgentLifecycle during AgentModule Ctor.");
}
}
private void RegisterListenerMethod(AgentLifecycleEventListener listener)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
return;
}
}
// Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
{
return;
}
}
this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
}
private void UnregisterListenerMethod(AgentLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners))
{
if (agentListeners.TryGetValue(listener.AgentId, out var agentListener))
{
agentListener.Remove(listener);
}
}
}
private void ReplaceVirtualTables(AgentModule* agentModule)
{
foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length))
{
try
{
var agentPointer = agentModule->Agents.GetPointer((int)index);
if (agentPointer is null)
{
Log.Warning("Null Agent Found?");
continue;
}
// AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, (AgentId)index, this));
}
catch (Exception e)
{
Log.Error(e, "Exception in AgentLifecycle during ReplaceVirtualTables.");
}
}
}
}
/// <summary>
/// Plugin-scoped version of a AgentLifecycle service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IAgentLifecycle>]
#pragma warning restore SA1015
internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle
{
[ServiceManager.ServiceDependency]
private readonly AgentLifecycle agentLifecycleService = Service<AgentLifecycle>.Get();
private readonly List<AgentLifecycleEventListener> eventListeners = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var listener in this.eventListeners)
{
this.agentLifecycleService.UnregisterListener(listener);
}
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate handler)
{
foreach (var agentId in agentIds)
{
this.RegisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate handler)
{
var listener = new AgentLifecycleEventListener(eventType, agentId, handler);
this.eventListeners.Add(listener);
this.agentLifecycleService.RegisterListener(listener);
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler)
{
this.RegisterListener(eventType, (AgentId)uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate? handler = null)
{
foreach (var agentId in agentIds)
{
this.UnregisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate? handler = null)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.EventType != eventType) return false;
if (entry.AgentId != agentId) return false;
if (handler is not null && entry.FunctionDelegate != handler) return false;
this.agentLifecycleService.UnregisterListener(entry);
return true;
});
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null)
{
this.UnregisterListener(eventType, (AgentId)uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(params IAgentLifecycle.AgentEventDelegate[] handlers)
{
foreach (var handler in handlers)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
this.agentLifecycleService.UnregisterListener(entry);
return true;
});
}
}
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)AgentLifecycle.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
}

View file

@ -1,43 +0,0 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Agent;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates.
/// </summary>
public class AgentLifecycleEventListener
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentLifecycleEventListener"/> class.
/// </summary>
/// <param name="eventType">Event type to listen for.</param>
/// <param name="agentId">Agent id to listen for.</param>
/// <param name="functionDelegate">Delegate to invoke.</param>
internal AgentLifecycleEventListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate functionDelegate)
{
this.EventType = eventType;
this.AgentId = agentId;
this.FunctionDelegate = functionDelegate;
}
/// <summary>
/// Gets the agentId of the agent this listener is looking for.
/// uint.MaxValue if it wants to be called for any agent.
/// </summary>
public AgentId AgentId { get; init; }
/// <summary>
/// Gets the event type this listener is looking for.
/// </summary>
public AgentEvent EventType { get; init; }
/// <summary>
/// Gets the delegate this listener invokes.
/// </summary>
public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; }
/// <summary>
/// Gets or sets if the listener is requested to be cleared.
/// </summary>
internal bool IsRequestedToClear { get; set; }
}

View file

@ -1,391 +0,0 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Agent.AgentArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Agent;
/// <summary>
/// Represents a class that holds references to an agents original and modified virtual table entries.
/// </summary>
internal unsafe class AgentVirtualTable : IDisposable
{
// This need to be at minimum the largest virtual table size of all agents
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 60;
private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("AgentVT");
private readonly AgentLifecycle lifecycleService;
private readonly AgentId agentId;
// Each agent gets its own set of args that are used to mutate the original call when used in pre-calls
private readonly AgentReceiveEventArgs receiveEventArgs = new();
private readonly AgentReceiveEventArgs filteredReceiveEventArgs = new();
private readonly AgentArgs showArgs = new();
private readonly AgentArgs hideArgs = new();
private readonly AgentArgs updateArgs = new();
private readonly AgentGameEventArgs gameEventArgs = new();
private readonly AgentLevelChangeArgs levelChangeArgs = new();
private readonly AgentClassJobChangeArgs classJobChangeArgs = new();
private readonly AgentInterface* agentInterface;
// 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 AgentInterface.Delegates.ReceiveEvent receiveEventFunction;
private readonly AgentInterface.Delegates.ReceiveEventWithResult receiveEventWithResultFunction;
private readonly AgentInterface.Delegates.Show showFunction;
private readonly AgentInterface.Delegates.Hide hideFunction;
private readonly AgentInterface.Delegates.Update updateFunction;
private readonly AgentInterface.Delegates.OnGameEvent gameEventFunction;
private readonly AgentInterface.Delegates.OnLevelChange levelChangeFunction;
private readonly AgentInterface.Delegates.OnClassJobChange classJobChangeFunction;
/// <summary>
/// Initializes a new instance of the <see cref="AgentVirtualTable"/> class.
/// </summary>
/// <param name="agent">AgentInterface* for the agent to replace the table of.</param>
/// <param name="agentId">Agent ID.</param>
/// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param>
internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService)
{
this.agentInterface = agent;
this.agentId = agentId;
this.lifecycleService = lifecycleService;
// Save original virtual table
this.OriginalVirtualTable = agent->VirtualTable;
// Create copy of original table
// Note this will copy any derived/overriden functions that this specific agent has.
// Note: currently there are 16 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
this.ModifiedVirtualTable = (AgentInterface.AgentInterfaceVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
NativeMemory.Copy(agent->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
// Overwrite the agents existing virtual table with our own
agent->VirtualTable = this.ModifiedVirtualTable;
// Pin each of our listener functions
this.receiveEventFunction = this.OnAgentReceiveEvent;
this.receiveEventWithResultFunction = this.OnAgentReceiveEventWithResult;
this.showFunction = this.OnAgentShow;
this.hideFunction = this.OnAgentHide;
this.updateFunction = this.OnAgentUpdate;
this.gameEventFunction = this.OnAgentGameEvent;
this.levelChangeFunction = this.OnAgentLevelChange;
this.classJobChangeFunction = this.OnClassJobChange;
// Overwrite specific virtual table entries
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
this.ModifiedVirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventWithResultFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged<AgentInterface*, AgentInterface.GameEvent, void>)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction);
this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged<AgentInterface*, byte, ushort, void>)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction);
this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged<AgentInterface*, byte, void>)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction);
}
/// <summary>
/// Gets the original virtual table address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
// Ensure restoration is done atomically.
Interlocked.Exchange(ref *(nint*)&this.agentInterface->VirtualTable, (nint)this.OriginalVirtualTable);
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
private AtkValue* OnAgentReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
{
AtkValue* result = null;
try
{
this.LogEvent(EnableLogging);
this.receiveEventArgs.Agent = thisPtr;
this.receiveEventArgs.AgentId = this.agentId;
this.receiveEventArgs.ReturnValue = (nint)returnValue;
this.receiveEventArgs.AtkValues = (nint)values;
this.receiveEventArgs.ValueCount = valueCount;
this.receiveEventArgs.EventKind = eventKind;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEvent, this.receiveEventArgs);
returnValue = (AtkValue*)this.receiveEventArgs.ReturnValue;
values = (AtkValue*)this.receiveEventArgs.AtkValues;
valueCount = this.receiveEventArgs.ValueCount;
eventKind = this.receiveEventArgs.EventKind;
try
{
result = this.OriginalVirtualTable->ReceiveEvent(thisPtr, returnValue, values, valueCount, eventKind);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Agent ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEvent, this.receiveEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEvent.");
}
return result;
}
private AtkValue* OnAgentReceiveEventWithResult(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
{
AtkValue* result = null;
try
{
this.LogEvent(EnableLogging);
this.filteredReceiveEventArgs.Agent = thisPtr;
this.filteredReceiveEventArgs.AgentId = this.agentId;
this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue;
this.filteredReceiveEventArgs.AtkValues = (nint)values;
this.filteredReceiveEventArgs.ValueCount = valueCount;
this.filteredReceiveEventArgs.EventKind = eventKind;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEventWithResult, this.filteredReceiveEventArgs);
returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue;
values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues;
valueCount = this.filteredReceiveEventArgs.ValueCount;
eventKind = this.filteredReceiveEventArgs.EventKind;
try
{
result = this.OriginalVirtualTable->ReceiveEventWithResult(thisPtr, returnValue, values, valueCount, eventKind);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEventWithResult, this.filteredReceiveEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEventWithResult.");
}
return result;
}
private void OnAgentShow(AgentInterface* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.showArgs.Agent = thisPtr;
this.showArgs.AgentId = this.agentId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs);
try
{
this.OriginalVirtualTable->Show(thisPtr);
}
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(AgentEvent.PostShow, this.showArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentShow.");
}
}
private void OnAgentHide(AgentInterface* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.hideArgs.Agent = thisPtr;
this.hideArgs.AgentId = this.agentId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs);
try
{
this.OriginalVirtualTable->Hide(thisPtr);
}
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(AgentEvent.PostHide, this.hideArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentHide.");
}
}
private void OnAgentUpdate(AgentInterface* thisPtr, uint frameCount)
{
try
{
this.LogEvent(EnableLogging);
this.updateArgs.Agent = thisPtr;
this.updateArgs.AgentId = this.agentId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs);
try
{
this.OriginalVirtualTable->Update(thisPtr, frameCount);
}
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(AgentEvent.PostUpdate, this.updateArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentUpdate.");
}
}
private void OnAgentGameEvent(AgentInterface* thisPtr, AgentInterface.GameEvent gameEvent)
{
try
{
this.LogEvent(EnableLogging);
this.gameEventArgs.Agent = thisPtr;
this.gameEventArgs.AgentId = this.agentId;
this.gameEventArgs.GameEvent = (int)gameEvent;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreGameEvent, this.gameEventArgs);
gameEvent = (AgentInterface.GameEvent)this.gameEventArgs.GameEvent;
try
{
this.OriginalVirtualTable->OnGameEvent(thisPtr, gameEvent);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnGameEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostGameEvent, this.gameEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentGameEvent.");
}
}
private void OnAgentLevelChange(AgentInterface* thisPtr, byte classJobId, ushort level)
{
try
{
this.LogEvent(EnableLogging);
this.levelChangeArgs.Agent = thisPtr;
this.levelChangeArgs.AgentId = this.agentId;
this.levelChangeArgs.ClassJobId = classJobId;
this.levelChangeArgs.Level = level;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreLevelChange, this.levelChangeArgs);
classJobId = this.levelChangeArgs.ClassJobId;
level = this.levelChangeArgs.Level;
try
{
this.OriginalVirtualTable->OnLevelChange(thisPtr, classJobId, level);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnLevelChange. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostLevelChange, this.levelChangeArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentLevelChange.");
}
}
private void OnClassJobChange(AgentInterface* thisPtr, byte classJobId)
{
try
{
this.LogEvent(EnableLogging);
this.classJobChangeArgs.Agent = thisPtr;
this.classJobChangeArgs.AgentId = this.agentId;
this.classJobChangeArgs.ClassJobId = classJobId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreClassJobChange, this.classJobChangeArgs);
classJobId = this.classJobChangeArgs.ClassJobId;
try
{
this.OriginalVirtualTable->OnClassJobChange(thisPtr, classJobId);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnClassJobChange. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostClassJobChange, this.classJobChangeArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnClassJobChange.");
}
}
[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 "OnAgentUpdate" || this.agentId is AgentId.PadMouseMode)
return;
Log.Debug($"[{caller}]: {this.agentId}");
}
}
}

View file

@ -2,8 +2,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Plugin.Services;
namespace Dalamud.Game;
/// <summary>
@ -14,7 +12,7 @@ public abstract class BaseAddressResolver
/// <summary>
/// Gets a list of memory addresses that were found, to list in /xldata.
/// </summary>
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = [];
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
/// <summary>
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(ISigScanner)"/> or <see cref="Setup64Bit(ISigScanner)"/>.

View file

@ -1,221 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using FFXIVClientStructs.Interop;
using Lumina.Excel;
using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Chat;
/// <summary>
/// Interface representing a log message.
/// </summary>
public interface ILogMessage : IEquatable<ILogMessage>
{
/// <summary>
/// Gets the address of the log message in memory.
/// </summary>
nint Address { get; }
/// <summary>
/// Gets the ID of this log message.
/// </summary>
uint LogMessageId { get; }
/// <summary>
/// Gets the GameData associated with this log message.
/// </summary>
RowRef<Lumina.Excel.Sheets.LogMessage> GameData { get; }
/// <summary>
/// Gets the entity that is the source of this log message, if any.
/// </summary>
ILogMessageEntity? SourceEntity { get; }
/// <summary>
/// Gets the entity that is the target of this log message, if any.
/// </summary>
ILogMessageEntity? TargetEntity { get; }
/// <summary>
/// Gets the number of parameters.
/// </summary>
int ParameterCount { get; }
/// <summary>
/// Retrieves the value of a parameter for the log message if it is an int.
/// </summary>
/// <param name="index">The index of the parameter to retrieve.</param>
/// <param name="value">The value of the parameter.</param>
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
bool TryGetIntParameter(int index, out int value);
/// <summary>
/// Retrieves the value of a parameter for the log message if it is a string.
/// </summary>
/// <param name="index">The index of the parameter to retrieve.</param>
/// <param name="value">The value of the parameter.</param>
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
bool TryGetStringParameter(int index, out ReadOnlySeString value);
/// <summary>
/// Formats this log message into an approximation of the string that will eventually be shown in the log.
/// </summary>
/// <remarks>This can cause side effects such as playing sound effects and thus should only be used for debugging.</remarks>
/// <returns>The formatted string.</returns>
ReadOnlySeString FormatLogMessageForDebugging();
}
/// <summary>
/// This struct represents log message in the queue to be added to the chat.
/// </summary>
/// <param name="ptr">A pointer to the log message.</param>
internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage
{
/// <inheritdoc/>
public nint Address => (nint)ptr;
/// <inheritdoc/>
public uint LogMessageId => ptr->LogMessageId;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.LogMessage> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.LogMessage>(ptr->LogMessageId);
/// <inheritdoc/>
ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
/// <inheritdoc/>
ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
/// <inheritdoc/>
public int ParameterCount => ptr->Parameters.Count;
private LogMessageEntity SourceEntity => new(ptr, true);
private LogMessageEntity TargetEntity => new(ptr, false);
public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y);
public static bool operator !=(LogMessage x, LogMessage y) => !(x == y);
/// <inheritdoc/>
public bool Equals(ILogMessage? other)
{
return other is LogMessage logMessage && this.Equals(logMessage);
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is LogMessage logMessage && this.Equals(logMessage);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity);
}
/// <inheritdoc/>
public bool TryGetIntParameter(int index, out int value)
{
value = 0;
if (!this.TryGetParameter(index, out var parameter)) return false;
if (parameter.Type != TextParameterType.Integer) return false;
value = parameter.IntValue;
return true;
}
/// <inheritdoc/>
public bool TryGetStringParameter(int index, out ReadOnlySeString value)
{
value = default;
if (!this.TryGetParameter(index, out var parameter)) return false;
if (parameter.Type == TextParameterType.String)
{
value = new(parameter.StringValue.AsSpan());
return true;
}
if (parameter.Type == TextParameterType.ReferencedUtf8String)
{
value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
return true;
}
return false;
}
/// <inheritdoc/>
public ReadOnlySeString FormatLogMessageForDebugging()
{
var logModule = RaptureLogModule.Instance();
// the formatting logic is taken from RaptureLogModule_Update
using var utf8 = new Utf8String();
SetName(logModule, this.SourceEntity);
SetName(logModule, this.TargetEntity);
using var rssb = new RentedSeStringBuilder();
logModule->RaptureTextModule->FormatString(rssb.Builder.Append(this.GameData.Value.Text).GetViewAsSpan(), &ptr->Parameters, &utf8);
return new ReadOnlySeString(utf8.AsSpan());
static void SetName(RaptureLogModule* self, LogMessageEntity item)
{
var name = item.NameSpan.GetPointer(0);
if (item.IsPlayer)
{
var str = self->TempParseMessage.GetPointer(item.IsSourceEntity ? 8 : 9);
self->FormatPlayerLink(name, str, null, 0, item.Kind != 1 /* LocalPlayer */, item.HomeWorldId, false, null, false);
if (item.HomeWorldId != 0 && item.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId)
{
var crossWorldSymbol = self->RaptureTextModule->UnkStrings0.GetPointer(3);
if (!crossWorldSymbol->StringPtr.HasValue)
self->RaptureTextModule->ProcessMacroCode(crossWorldSymbol, "<icon(88)>\0"u8);
str->Append(crossWorldSymbol);
if (self->UIModule->GetWorldHelper()->AllWorlds.TryGetValuePointer(item.HomeWorldId, out var world))
str->ConcatCStr(world->Name);
}
name = str->StringPtr;
}
if (item.IsSourceEntity)
{
self->RaptureTextModule->SetGlobalTempEntity1(name, item.Sex, item.ObjStrId);
}
else
{
self->RaptureTextModule->SetGlobalTempEntity2(name, item.Sex, item.ObjStrId);
}
}
}
private bool TryGetParameter(int index, out TextParameter value)
{
if (index < 0 || index >= ptr->Parameters.Count)
{
value = default;
return false;
}
value = ptr->Parameters[index];
return true;
}
private bool Equals(LogMessage other)
{
return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity;
}
}

View file

@ -1,113 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Chat;
/// <summary>
/// Interface representing an entity related to a log message.
/// </summary>
public interface ILogMessageEntity : IEquatable<ILogMessageEntity>
{
/// <summary>
/// Gets the name of this entity.
/// </summary>
ReadOnlySeString Name { get; }
/// <summary>
/// Gets the ID of the homeworld of this entity, if it is a player.
/// </summary>
ushort HomeWorldId { get; }
/// <summary>
/// Gets the homeworld of this entity, if it is a player.
/// </summary>
RowRef<World> HomeWorld { get; }
/// <summary>
/// Gets the ObjStr ID of this entity, if not a player. See <seealso cref="ISeStringEvaluator.EvaluateObjStr"/>.
/// </summary>
uint ObjStrId { get; }
/// <summary>
/// Gets a value indicating whether this entity is a player.
/// </summary>
bool IsPlayer { get; }
}
/// <summary>
/// This struct represents an entity related to a log message.
/// </summary>
/// <param name="ptr">A pointer to the log message item.</param>
/// <param name="source">If <see langword="true"/> represents the source entity of the log message, otherwise represents the target entity.</param>
internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
{
/// <inheritdoc/>
public ReadOnlySeString Name => new(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
/// <inheritdoc/>
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
/// <inheritdoc/>
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.HomeWorldId);
/// <inheritdoc/>
public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId;
/// <inheritdoc/>
public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
/// <summary>
/// Gets the Span containing the raw name of this entity.
/// </summary>
internal Span<byte> NameSpan => source ? ptr->SourceName : ptr->TargetName;
/// <summary>
/// Gets the kind of the entity.
/// </summary>
internal byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
/// <summary>
/// Gets the Sex of this entity.
/// </summary>
internal byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
/// <summary>
/// Gets a value indicating whether this entity is the source entity of a log message.
/// </summary>
internal bool IsSourceEntity => source;
public static bool operator ==(LogMessageEntity x, LogMessageEntity y) => x.Equals(y);
public static bool operator !=(LogMessageEntity x, LogMessageEntity y) => !(x == y);
/// <inheritdoc/>
public bool Equals(ILogMessageEntity other)
{
return other is LogMessageEntity entity && this.Equals(entity);
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is LogMessageEntity entity && this.Equals(entity);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer);
}
private bool Equals(LogMessageEntity other)
{
return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer;
}
}

View file

@ -1,4 +1,5 @@
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using CheapLoc;
@ -22,7 +23,7 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService]
internal partial class ChatHandlers : IServiceType
{
private static readonly ModuleLog Log = ModuleLog.Create<ChatHandlers>();
private static readonly ModuleLog Log = new("ChatHandlers");
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -103,7 +104,7 @@ internal partial class ChatHandlers : IServiceType
if (this.configuration.PrintDalamudWelcomeMsg)
{
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion())
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
}
@ -115,7 +116,7 @@ internal partial class ChatHandlers : IServiceType
}
}
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion))
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
{
var linkPayload = chatGui.AddChatLinkHandler(
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
@ -136,7 +137,7 @@ internal partial class ChatHandlers : IServiceType
Type = XivChatType.Notice,
});
this.configuration.LastVersion = Versioning.GetAssemblyVersion();
this.configuration.LastVersion = Util.AssemblyVersion;
this.configuration.QueueSave();
}

View file

@ -63,37 +63,47 @@ public interface IAetheryteEntry
}
/// <summary>
/// This struct represents an aetheryte entry available to the game.
/// Class representing an aetheryte entry available to the game.
/// </summary>
/// <param name="data">Data read from the Aetheryte List.</param>
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
internal sealed class AetheryteEntry : IAetheryteEntry
{
/// <inheritdoc />
public uint AetheryteId => data.AetheryteId;
private readonly TeleportInfo data;
/// <summary>
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
/// </summary>
/// <param name="data">Data read from the Aetheryte List.</param>
internal AetheryteEntry(TeleportInfo data)
{
this.data = data;
}
/// <inheritdoc />
public uint TerritoryId => data.TerritoryId;
public uint AetheryteId => this.data.AetheryteId;
/// <inheritdoc />
public byte SubIndex => data.SubIndex;
public uint TerritoryId => this.data.TerritoryId;
/// <inheritdoc />
public byte Ward => data.Ward;
public byte SubIndex => this.data.SubIndex;
/// <inheritdoc />
public byte Plot => data.Plot;
public byte Ward => this.data.Ward;
/// <inheritdoc />
public uint GilCost => data.GilCost;
public byte Plot => this.data.Plot;
/// <inheritdoc />
public bool IsFavourite => data.IsFavourite;
public uint GilCost => this.data.GilCost;
/// <inheritdoc />
public bool IsSharedHouse => data.IsSharedHouse;
public bool IsFavourite => this.data.IsFavourite;
/// <inheritdoc />
public bool IsApartment => data.IsApartment;
public bool IsSharedHouse => this.data.IsSharedHouse;
/// <inheritdoc />
public bool IsApartment => this.data.IsApartment;
/// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);

View file

@ -8,7 +8,6 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog;
namespace Dalamud.Game.ClientState.Aetherytes;
@ -89,7 +88,10 @@ internal sealed partial class AetheryteList
/// <inheritdoc/>
public IEnumerator<IAetheryteEntry> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
@ -97,34 +99,4 @@ 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()
{
}
}
}

View file

@ -8,7 +8,6 @@ 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;
@ -24,7 +23,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning restore SA1015
internal sealed partial class BuddyList : IServiceType, IBuddyList
{
private const uint InvalidEntityId = 0xE0000000;
private const uint InvalidObjectID = 0xE0000000;
[ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get();
@ -85,37 +84,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
/// <inheritdoc/>
public unsafe nint GetCompanionBuddyMemberAddress()
public unsafe IntPtr GetCompanionBuddyMemberAddress()
{
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
}
/// <inheritdoc/>
public unsafe nint GetPetBuddyMemberAddress()
public unsafe IntPtr GetPetBuddyMemberAddress()
{
return (nint)this.BuddyListStruct->PetInfo.Pet;
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
}
/// <inheritdoc/>
public unsafe nint GetBattleBuddyMemberAddress(int index)
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
{
if (index < 0 || index >= 3)
return 0;
return IntPtr.Zero;
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
}
/// <inheritdoc/>
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
{
if (address == 0)
if (address == IntPtr.Zero)
return null;
if (this.playerState.ContentId == 0)
if (!this.playerState.IsLoaded)
return null;
var buddy = new BuddyMember((CSBuddyMember*)address);
if (buddy.EntityId == InvalidEntityId)
var buddy = new BuddyMember(address);
if (buddy.ObjectId == InvalidObjectID)
return null;
return buddy;
@ -133,39 +132,12 @@ internal sealed partial class BuddyList
/// <inheritdoc/>
public IEnumerator<IBuddyMember> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <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()
{
}
}
}

View file

@ -1,24 +1,20 @@
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 : IEquatable<IBuddyMember>
public interface IBuddyMember
{
/// <summary>
/// Gets the address of the buddy in memory.
/// </summary>
nint Address { get; }
IntPtr Address { get; }
/// <summary>
/// Gets the object ID of this buddy.
@ -71,34 +67,42 @@ public interface IBuddyMember : IEquatable<IBuddyMember>
}
/// <summary>
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary>
/// <param name="ptr">A pointer to the BuddyMember.</param>
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
internal unsafe class BuddyMember : IBuddyMember
{
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <inheritdoc />
public nint Address => (nint)ptr;
/// <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 uint ObjectId => this.EntityId;
public IntPtr Address { get; }
/// <inheritdoc />
public uint EntityId => ptr->EntityId;
public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc />
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc />
public uint CurrentHP => ptr->CurrentHealth;
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
/// <inheritdoc />
public uint MaxHP => ptr->MaxHealth;
public uint CurrentHP => this.Struct->CurrentHealth;
/// <inheritdoc />
public uint DataID => ptr->DataId;
public uint MaxHP => this.Struct->MaxHealth;
/// <inheritdoc />
public uint DataID => this.Struct->DataId;
/// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
@ -109,25 +113,5 @@ internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
/// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
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();
}
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
}

View file

@ -33,10 +33,11 @@ namespace Dalamud.Game.ClientState;
[ServiceManager.EarlyLoadedService]
internal sealed class ClientState : IInternalDisposableService, IClientState
{
private static readonly ModuleLog Log = ModuleLog.Create<ClientState>();
private static readonly ModuleLog Log = new("ClientState");
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;
private readonly Hook<HandleZoneInitPacketDelegate> handleZoneInitPacketHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<SetCurrentInstanceDelegate> setCurrentInstanceHook;
@ -71,11 +72,13 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
this.handleZoneInitPacketHook = Hook<HandleZoneInitPacketDelegate>.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.setCurrentInstanceHook = Hook<SetCurrentInstanceDelegate>.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour);
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.handleZoneInitPacketHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.setCurrentInstanceHook.Enable();
@ -268,6 +271,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// </summary>
void IInternalDisposableService.DisposeService()
{
this.handleZoneInitPacketHook.Dispose();
this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose();
this.setCurrentInstanceHook.Dispose();
@ -290,6 +294,23 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.framework.Update += this.OnFrameworkUpdate;
}
private void HandleZoneInitPacketDetour(nint a1, uint localPlayerEntityId, nint packet, byte type)
{
this.handleZoneInitPacketHook.Original(a1, localPlayerEntityId, packet, type);
try
{
var eventArgs = ZoneInitEventArgs.Read(packet);
Log.Debug($"ZoneInit: {eventArgs}");
this.ZoneInit?.InvokeSafely(eventArgs);
this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId;
}
catch (Exception ex)
{
Log.Error(ex, "Exception during ZoneInit");
}
}
private unsafe void UIModuleHandlePacketDetour(
UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{
@ -335,15 +356,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
break;
}
case (UIModulePacketType)5: // TODO: Use UIModulePacketType.InitZone when available
{
var eventArgs = ZoneInitEventArgs.Read((nint)packet);
Log.Debug($"ZoneInit: {eventArgs}");
this.ZoneInit?.InvokeSafely(eventArgs);
this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId;
break;
}
}
}

View file

@ -1,5 +1,3 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.ClientState;
/// <summary>
@ -21,6 +19,11 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
// Functions
/// <summary>
/// Gets the address of the method that handles the ZoneInit packet.
/// </summary>
public nint HandleZoneInitPacket { get; private set; }
/// <summary>
/// Gets the address of the method that sets the current public instance.
/// </summary>
@ -32,6 +35,7 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.HandleZoneInitPacket = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 45");
this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance
// These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used.

View file

@ -18,7 +18,7 @@ internal sealed class Condition : IInternalDisposableService, ICondition
/// <summary>
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
/// </summary>
internal const int MaxConditionEntries = 112;
internal const int MaxConditionEntries = 104;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();

View file

@ -520,17 +520,4 @@ public enum ConditionFlag
PilotingMech = 102,
// Unknown103 = 103,
/// <summary>
/// Unable to execute command while editing a strategy board.
/// </summary>
EditingStrategyBoard = 104,
// Unknown105 = 105,
// Unknown106 = 106,
// Unknown107 = 107,
// Unknown108 = 108,
// Unknown109 = 109,
// Unknown110 = 110,
// Unknown111 = 111,
}

View file

@ -1,311 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Customize;
/// <summary>
/// This collection represents customization data a <see cref="ICharacter"/> has.
/// </summary>
public interface ICustomizeData
{
/// <summary>
/// Gets the current race.
/// E.g., Miqo'te, Aura.
/// </summary>
public byte Race { get; }
/// <summary>
/// Gets the current sex.
/// </summary>
public byte Sex { get; }
/// <summary>
/// Gets the current body type.
/// </summary>
public byte BodyType { get; }
/// <summary>
/// Gets the current height (0 to 100).
/// </summary>
public byte Height { get; }
/// <summary>
/// Gets the current tribe.
/// E.g., Seeker of the Sun, Keeper of the Moon.
/// </summary>
public byte Tribe { get; }
/// <summary>
/// Gets the current face (1 to 4).
/// </summary>
public byte Face { get; }
/// <summary>
/// Gets the current hairstyle.
/// </summary>
public byte Hairstyle { get; }
/// <summary>
/// Gets the current skin color.
/// </summary>
public byte SkinColor { get; }
/// <summary>
/// Gets the current color of the left eye.
/// </summary>
public byte EyeColorLeft { get; }
/// <summary>
/// Gets the current color of the right eye.
/// </summary>
public byte EyeColorRight { get; }
/// <summary>
/// Gets the current main hair color.
/// </summary>
public byte HairColor { get; }
/// <summary>
/// Gets the current highlight hair color.
/// </summary>
public byte HighlightsColor { get; }
/// <summary>
/// Gets the current tattoo color.
/// </summary>
public byte TattooColor { get; }
/// <summary>
/// Gets the current eyebrow type.
/// </summary>
public byte Eyebrows { get; }
/// <summary>
/// Gets the current nose type.
/// </summary>
public byte Nose { get; }
/// <summary>
/// Gets the current jaw type.
/// </summary>
public byte Jaw { get; }
/// <summary>
/// Gets the current lip color fur pattern.
/// </summary>
public byte LipColorFurPattern { get; }
/// <summary>
/// Gets the current muscle mass value.
/// </summary>
public byte MuscleMass { get; }
/// <summary>
/// Gets the current tail type (1 to 4).
/// </summary>
public byte TailShape { get; }
/// <summary>
/// Gets the current bust size (0 to 100).
/// </summary>
public byte BustSize { get; }
/// <summary>
/// Gets the current color of the face paint.
/// </summary>
public byte FacePaintColor { get; }
/// <summary>
/// Gets a value indicating whether highlight color is used.
/// </summary>
public bool Highlights { get; }
/// <summary>
/// Gets a value indicating whether this facial feature is used.
/// </summary>
public bool FacialFeature1 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature2 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature3 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature4 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature5 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature6 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature7 { get; }
/// <summary>
/// Gets a value indicating whether the legacy tattoo is used.
/// </summary>
public bool LegacyTattoo { get; }
/// <summary>
/// Gets the current eye shape type.
/// </summary>
public byte EyeShape { get; }
/// <summary>
/// Gets a value indicating whether small iris is used.
/// </summary>
public bool SmallIris { get; }
/// <summary>
/// Gets the current mouth type.
/// </summary>
public byte Mouth { get; }
/// <summary>
/// Gets a value indicating whether lipstick is used.
/// </summary>
public bool Lipstick { get; }
/// <summary>
/// Gets the current face paint type.
/// </summary>
public byte FacePaint { get; }
/// <summary>
/// Gets a value indicating whether face paint reversed is used.
/// </summary>
public bool FacePaintReversed { get; }
}
/// <inheritdoc/>
internal readonly unsafe struct CustomizeData : ICustomizeData
{
/// <summary>
/// Gets or sets the address of the customize data struct in memory.
/// </summary>
public readonly nint Address;
/// <summary>
/// Initializes a new instance of the <see cref="CustomizeData"/> struct.
/// </summary>
/// <param name="address">Address of the status list.</param>
internal CustomizeData(nint address)
{
this.Address = address;
}
/// <inheritdoc/>
public byte Race => this.Struct->Race;
/// <inheritdoc/>
public byte Sex => this.Struct->Sex;
/// <inheritdoc/>
public byte BodyType => this.Struct->BodyType;
/// <inheritdoc/>
public byte Height => this.Struct->Height;
/// <inheritdoc/>
public byte Tribe => this.Struct->Tribe;
/// <inheritdoc/>
public byte Face => this.Struct->Face;
/// <inheritdoc/>
public byte Hairstyle => this.Struct->Hairstyle;
/// <inheritdoc/>
public byte SkinColor => this.Struct->SkinColor;
/// <inheritdoc/>
public byte EyeColorLeft => this.Struct->EyeColorLeft;
/// <inheritdoc/>
public byte EyeColorRight => this.Struct->EyeColorRight;
/// <inheritdoc/>
public byte HairColor => this.Struct->HairColor;
/// <inheritdoc/>
public byte HighlightsColor => this.Struct->HighlightsColor;
/// <inheritdoc/>
public byte TattooColor => this.Struct->TattooColor;
/// <inheritdoc/>
public byte Eyebrows => this.Struct->Eyebrows;
/// <inheritdoc/>
public byte Nose => this.Struct->Nose;
/// <inheritdoc/>
public byte Jaw => this.Struct->Jaw;
/// <inheritdoc/>
public byte LipColorFurPattern => this.Struct->LipColorFurPattern;
/// <inheritdoc/>
public byte MuscleMass => this.Struct->MuscleMass;
/// <inheritdoc/>
public byte TailShape => this.Struct->TailShape;
/// <inheritdoc/>
public byte BustSize => this.Struct->BustSize;
/// <inheritdoc/>
public byte FacePaintColor => this.Struct->FacePaintColor;
/// <inheritdoc/>
public bool Highlights => this.Struct->Highlights;
/// <inheritdoc/>
public bool FacialFeature1 => this.Struct->FacialFeature1;
/// <inheritdoc/>
public bool FacialFeature2 => this.Struct->FacialFeature2;
/// <inheritdoc/>
public bool FacialFeature3 => this.Struct->FacialFeature3;
/// <inheritdoc/>
public bool FacialFeature4 => this.Struct->FacialFeature4;
/// <inheritdoc/>
public bool FacialFeature5 => this.Struct->FacialFeature5;
/// <inheritdoc/>
public bool FacialFeature6 => this.Struct->FacialFeature6;
/// <inheritdoc/>
public bool FacialFeature7 => this.Struct->FacialFeature7;
/// <inheritdoc/>
public bool LegacyTattoo => this.Struct->LegacyTattoo;
/// <inheritdoc/>
public byte EyeShape => this.Struct->EyeShape;
/// <inheritdoc/>
public bool SmallIris => this.Struct->SmallIris;
/// <inheritdoc/>
public byte Mouth => this.Struct->Mouth;
/// <inheritdoc/>
public bool Lipstick => this.Struct->Lipstick;
/// <inheritdoc/>
public byte FacePaint => this.Struct->FacePaint;
/// <inheritdoc/>
public bool FacePaintReversed => this.Struct->FacePaintReversed;
/// <summary>
/// Gets the underlying structure.
/// </summary>
internal FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData* Struct =>
(FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData*)this.Address;
}

View file

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Dalamud.Data;
@ -8,12 +7,10 @@ using Dalamud.Memory;
using Lumina.Excel;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
namespace Dalamud.Game.ClientState.Fates;
/// <summary>
/// Interface representing a fate entry that can be seen in the current area.
/// Interface representing an fate entry that can be seen in the current area.
/// </summary>
public interface IFate : IEquatable<IFate>
{
@ -115,96 +112,129 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets the address of this Fate in memory.
/// </summary>
nint Address { get; }
IntPtr Address { get; }
}
/// <summary>
/// This struct represents a Fate.
/// This class represents an FFXIV Fate.
/// </summary>
/// <param name="ptr">A pointer to the FateContext.</param>
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
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 nint Address => (nint)ptr;
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/>
public ushort FateId => ptr->FateId;
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
{
/// <inheritdoc/>
public ushort FateId => this.Struct->FateId;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
/// <inheritdoc/>
public int StartTimeEpoch => ptr->StartTimeEpoch;
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
/// <inheritdoc/>
public short Duration => ptr->Duration;
public short Duration => this.Struct->Duration;
/// <inheritdoc/>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
/// <inheritdoc/>
public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
/// <inheritdoc/>
public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
/// <inheritdoc/>
public FateState State => (FateState)ptr->State;
public FateState State => (FateState)this.Struct->State;
/// <inheritdoc/>
public byte HandInCount => ptr->HandInCount;
public byte HandInCount => this.Struct->HandInCount;
/// <inheritdoc/>
public byte Progress => ptr->Progress;
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
public bool HasBonus => ptr->IsBonus;
public bool HasBonus => this.Struct->IsBonus;
/// <inheritdoc/>
public uint IconId => ptr->IconId;
public uint IconId => this.Struct->IconId;
/// <inheritdoc/>
public byte Level => ptr->Level;
public byte Level => this.Struct->Level;
/// <inheritdoc/>
public byte MaxLevel => ptr->MaxLevel;
public byte MaxLevel => this.Struct->MaxLevel;
/// <inheritdoc/>
public Vector3 Position => ptr->Location;
public Vector3 Position => this.Struct->Location;
/// <inheritdoc/>
public float Radius => ptr->Radius;
public float Radius => this.Struct->Radius;
/// <inheritdoc/>
public uint MapIconId => ptr->MapIconId;
public uint MapIconId => this.Struct->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>(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();
}
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
}

View file

@ -6,7 +6,6 @@ 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;
@ -27,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
}
/// <inheritdoc/>
public unsafe nint Address => (nint)CSFateManager.Instance();
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/>
public unsafe int Length
@ -70,29 +69,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
}
/// <inheritdoc/>
public unsafe nint GetFateAddress(int index)
public unsafe IntPtr GetFateAddress(int index)
{
if (index >= this.Length)
return 0;
return IntPtr.Zero;
var fateManager = CSFateManager.Instance();
if (fateManager == null)
return 0;
return IntPtr.Zero;
return (nint)fateManager->Fates[index].Value;
return (IntPtr)fateManager->Fates[index].Value;
}
/// <inheritdoc/>
public unsafe IFate? CreateFateReference(IntPtr address)
public IFate? CreateFateReference(IntPtr offset)
{
if (address == 0)
if (offset == IntPtr.Zero)
return null;
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new Fate((CSFateContext*)address);
return new Fate(offset);
}
}
@ -107,39 +106,12 @@ internal sealed partial class FateTable
/// <inheritdoc/>
public IEnumerator<IFate> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <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()
{
}
}
}

View file

@ -5,9 +5,7 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using Serilog;
namespace Dalamud.Game.ClientState.GamePad;

View file

@ -37,7 +37,7 @@ internal class JobGauges : IServiceType, IJobGauges
// Since the gauge itself reads from live memory, there isn't much downside to doing this.
if (!this.cache.TryGetValue(typeof(T), out var gauge))
{
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, [this.Address], null);
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.Address }, null);
}
return (T)gauge;

View file

@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types;
@ -83,12 +82,12 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
{
get
{
return
[
return new[]
{
this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.Mage : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.Army : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.Wanderer : Song.None,
];
};
}
}
}

View file

@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types;

View file

@ -1,4 +1,4 @@
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using CanvasFlags = Dalamud.Game.ClientState.JobGauge.Enums.CanvasFlags;
using CreatureFlags = Dalamud.Game.ClientState.JobGauge.Enums.CreatureFlags;
@ -22,45 +22,45 @@ public unsafe class PCTGauge : JobGaugeBase<PictomancerGauge>
/// <summary>
/// Gets the use of subjective pallete.
/// </summary>
public byte PalleteGauge => this.Struct->PalleteGauge;
public byte PalleteGauge => Struct->PalleteGauge;
/// <summary>
/// Gets the amount of paint the player has.
/// </summary>
public byte Paint => this.Struct->Paint;
public byte Paint => Struct->Paint;
/// <summary>
/// Gets a value indicating whether a creature motif is drawn.
/// </summary>
public bool CreatureMotifDrawn => this.Struct->CreatureMotifDrawn;
public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn;
/// <summary>
/// Gets a value indicating whether a weapon motif is drawn.
/// </summary>
public bool WeaponMotifDrawn => this.Struct->WeaponMotifDrawn;
public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn;
/// <summary>
/// Gets a value indicating whether a landscape motif is drawn.
/// </summary>
public bool LandscapeMotifDrawn => this.Struct->LandscapeMotifDrawn;
public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn;
/// <summary>
/// Gets a value indicating whether a moogle portrait is ready.
/// </summary>
public bool MooglePortraitReady => this.Struct->MooglePortraitReady;
public bool MooglePortraitReady => Struct->MooglePortraitReady;
/// <summary>
/// Gets a value indicating whether a madeen portrait is ready.
/// </summary>
public bool MadeenPortraitReady => this.Struct->MadeenPortraitReady;
public bool MadeenPortraitReady => Struct->MadeenPortraitReady;
/// <summary>
/// Gets which creature flags are present.
/// </summary>
public CreatureFlags CreatureFlags => (CreatureFlags)this.Struct->CreatureFlags;
public CreatureFlags CreatureFlags => (CreatureFlags)Struct->CreatureFlags;
/// <summary>
/// Gets which canvas flags are present.
/// </summary>
public CanvasFlags CanvasFlags => (CanvasFlags)this.Struct->CanvasFlags;
public CanvasFlags CanvasFlags => (CanvasFlags)Struct->CanvasFlags;
}

View file

@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types;

View file

@ -1,5 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using Reloaded.Memory;
using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo;
using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo;
@ -22,25 +24,25 @@ public unsafe class VPRGauge : JobGaugeBase<ViperGauge>
/// <summary>
/// Gets how many uses of uncoiled fury the player has.
/// </summary>
public byte RattlingCoilStacks => this.Struct->RattlingCoilStacks;
public byte RattlingCoilStacks => Struct->RattlingCoilStacks;
/// <summary>
/// Gets Serpent Offering stacks and gauge.
/// </summary>
public byte SerpentOffering => this.Struct->SerpentOffering;
public byte SerpentOffering => Struct->SerpentOffering;
/// <summary>
/// Gets value indicating the use of 1st, 2nd, 3rd, 4th generation and Ouroboros.
/// </summary>
public byte AnguineTribute => this.Struct->AnguineTribute;
public byte AnguineTribute => Struct->AnguineTribute;
/// <summary>
/// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo.
/// </summary>
public DreadCombo DreadCombo => (DreadCombo)this.Struct->DreadCombo;
public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo;
/// <summary>
/// Gets current ability for Serpent's Tail.
/// </summary>
public SerpentCombo SerpentCombo => (SerpentCombo)this.Struct->SerpentCombo;
public SerpentCombo SerpentCombo => (SerpentCombo)Struct->SerpentCombo;
}

View file

@ -13,6 +13,8 @@ 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;
@ -35,6 +37,8 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
private readonly CachedEntry[] cachedObjectTable;
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable()
{
@ -44,6 +48,9 @@ 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/>
@ -236,25 +243,43 @@ internal sealed partial class ObjectTable
public IEnumerator<IGameObject> GetEnumerator()
{
ThreadSafety.AssertMainThread();
return new Enumerator(this);
// 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);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(ObjectTable owner) : IEnumerator<IGameObject>
private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable
{
private ObjectTable? owner = owner;
private int index = -1;
public IGameObject Current { get; private set; }
public IGameObject Current { get; private set; } = null!;
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
var cache = owner.cachedObjectTable.AsSpan();
if (this.index == objectTableLength)
return false;
while (++this.index < objectTableLength)
var cache = this.owner!.cachedObjectTable.AsSpan();
for (this.index++; this.index < objectTableLength; this.index++)
{
if (cache[this.index].Update() is { } ao)
{
@ -263,17 +288,24 @@ 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;
}
}
}

View file

@ -1,7 +1,6 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
@ -30,50 +29,50 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager
/// <inheritdoc/>
public IGameObject? Target
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetHardTarget());
set => this.Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetHardTarget());
set => Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
}
/// <inheritdoc/>
public IGameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverTarget);
set => this.Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->FocusTarget);
set => this.Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->PreviousTarget);
set => this.Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? SoftTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetSoftTarget());
set => this.Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetSoftTarget());
set => Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
}
/// <inheritdoc/>
public IGameObject? GPoseTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GPoseTarget);
set => this.Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget);
set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? MouseOverNameplateTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverNameplateTarget);
set => this.Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget);
set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
private TargetSystem* Struct => TargetSystem.Instance();

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Utility;
namespace Dalamud.Game.ClientState.Objects.Types;

View file

@ -1,8 +1,9 @@
using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Customize;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using Dalamud.Memory;
using Lumina.Excel;
using Lumina.Excel.Sheets;
@ -15,73 +16,68 @@ namespace Dalamud.Game.ClientState.Objects.Types;
public interface ICharacter : IGameObject
{
/// <summary>
/// Gets the current HP of this character.
/// Gets the current HP of this Chara.
/// </summary>
public uint CurrentHp { get; }
/// <summary>
/// Gets the maximum HP of this character.
/// Gets the maximum HP of this Chara.
/// </summary>
public uint MaxHp { get; }
/// <summary>
/// Gets the current MP of this character.
/// Gets the current MP of this Chara.
/// </summary>
public uint CurrentMp { get; }
/// <summary>
/// Gets the maximum MP of this character.
/// Gets the maximum MP of this Chara.
/// </summary>
public uint MaxMp { get; }
/// <summary>
/// Gets the current GP of this character.
/// Gets the current GP of this Chara.
/// </summary>
public uint CurrentGp { get; }
/// <summary>
/// Gets the maximum GP of this character.
/// Gets the maximum GP of this Chara.
/// </summary>
public uint MaxGp { get; }
/// <summary>
/// Gets the current CP of this character.
/// Gets the current CP of this Chara.
/// </summary>
public uint CurrentCp { get; }
/// <summary>
/// Gets the maximum CP of this character.
/// Gets the maximum CP of this Chara.
/// </summary>
public uint MaxCp { get; }
/// <summary>
/// Gets the shield percentage of this character.
/// Gets the shield percentage of this Chara.
/// </summary>
public byte ShieldPercentage { get; }
/// <summary>
/// Gets the ClassJob of this character.
/// Gets the ClassJob of this Chara.
/// </summary>
public RowRef<ClassJob> ClassJob { get; }
/// <summary>
/// Gets the level of this character.
/// Gets the level of this Chara.
/// </summary>
public byte Level { get; }
/// <summary>
/// Gets a byte array describing the visual appearance of this character.
/// Gets a byte array describing the visual appearance of this Chara.
/// Indexed by <see cref="CustomizeIndex"/>.
/// </summary>
public byte[] Customize { get; }
/// <summary>
/// Gets the underlying CustomizeData struct for this character.
/// </summary>
public ICustomizeData CustomizeData { get; }
/// <summary>
/// Gets the Free Company tag of this character.
/// Gets the Free Company tag of this chara.
/// </summary>
public SeString CompanyTag { get; }
@ -123,7 +119,7 @@ internal unsafe class Character : GameObject, ICharacter
/// This represents a non-static entity.
/// </summary>
/// <param name="address">The address of this character in memory.</param>
internal Character(nint address)
internal Character(IntPtr address)
: base(address)
{
}
@ -162,12 +158,8 @@ internal unsafe class Character : GameObject, ICharacter
public byte Level => this.Struct->CharacterData.Level;
/// <inheritdoc/>
[Api15ToDo("Do not allocate on each call, use the CS Span and let consumers do allocation if necessary")]
public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray();
/// <inheritdoc/>
public ICustomizeData CustomizeData => new CustomizeData((nint)(&this.Struct->DrawData.CustomizeData));
/// <inheritdoc/>
public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);

View file

@ -9,7 +9,6 @@ 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;
@ -44,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
/// <inheritdoc/>
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/>
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.PartyMembers[0]);
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
/// <inheritdoc/>
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
/// <inheritdoc/>
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
private static int PartyMemberSize { get; } = Marshal.SizeOf<CSPartyMember>();
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress;
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
/// <inheritdoc/>
public IPartyMember? this[int index]
@ -82,45 +81,39 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
}
/// <inheritdoc/>
public nint GetPartyMemberAddress(int index)
public IntPtr GetPartyMemberAddress(int index)
{
if (index < 0 || index >= GroupLength)
return 0;
return IntPtr.Zero;
return this.GroupListAddress + (index * PartyMemberSize);
}
/// <inheritdoc/>
public IPartyMember? CreatePartyMemberReference(nint address)
public IPartyMember? CreatePartyMemberReference(IntPtr address)
{
if (this.playerState.ContentId == 0)
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
return null;
if (address == 0)
return null;
return new PartyMember((CSPartyMember*)address);
return new PartyMember(address);
}
/// <inheritdoc/>
public nint GetAllianceMemberAddress(int index)
public IntPtr GetAllianceMemberAddress(int index)
{
if (index < 0 || index >= AllianceLength)
return 0;
return IntPtr.Zero;
return this.AllianceListAddress + (index * PartyMemberSize);
}
/// <inheritdoc/>
public IPartyMember? CreateAllianceMemberReference(nint address)
public IPartyMember? CreateAllianceMemberReference(IntPtr address)
{
if (this.playerState.ContentId == 0)
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
return null;
if (address == 0)
return null;
return new PartyMember((CSPartyMember*)address);
return new PartyMember(address);
}
}
@ -135,43 +128,18 @@ internal sealed partial class PartyList
/// <inheritdoc/>
public IEnumerator<IPartyMember> GetEnumerator()
{
return new Enumerator(this);
// 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;
}
}
/// <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()
{
}
}
}

View file

@ -1,29 +1,26 @@
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.Utility;
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 : IEquatable<IPartyMember>
public interface IPartyMember
{
/// <summary>
/// Gets the address of this party member in memory.
/// </summary>
nint Address { get; }
IntPtr Address { get; }
/// <summary>
/// Gets a list of buffs or debuffs applied to this party member.
@ -111,82 +108,69 @@ public interface IPartyMember : IEquatable<IPartyMember>
}
/// <summary>
/// This struct represents a party member in the group manager.
/// This class represents a party member in the group manager.
/// </summary>
/// <param name="ptr">A pointer to the PartyMember.</param>
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
internal unsafe class PartyMember : IPartyMember
{
/// <inheritdoc/>
public nint Address => (nint)ptr;
/// <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 StatusList Statuses => new(&ptr->StatusManager);
public IntPtr Address { get; }
/// <inheritdoc/>
public Vector3 Position => ptr->Position;
public StatusList Statuses => new(&this.Struct->StatusManager);
/// <inheritdoc/>
[Api15ToDo("Change type to ulong.")]
public long ContentId => (long)ptr->ContentId;
public Vector3 Position => this.Struct->Position;
/// <inheritdoc/>
public uint ObjectId => ptr->EntityId;
public long ContentId => (long)this.Struct->ContentId;
/// <inheritdoc/>
public uint EntityId => ptr->EntityId;
public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc/>
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc/>
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
/// <inheritdoc/>
public uint CurrentHP => ptr->CurrentHP;
public uint CurrentHP => this.Struct->CurrentHP;
/// <inheritdoc/>
public uint MaxHP => ptr->MaxHP;
public uint MaxHP => this.Struct->MaxHP;
/// <inheritdoc/>
public ushort CurrentMP => ptr->CurrentMP;
public ushort CurrentMP => this.Struct->CurrentMP;
/// <inheritdoc/>
public ushort MaxMP => ptr->MaxMP;
public ushort MaxMP => this.Struct->MaxMP;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->TerritoryType);
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(ptr->HomeWorld);
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
/// <inheritdoc/>
public SeString Name => SeString.Parse(ptr->Name);
public SeString Name => SeString.Parse(this.Struct->Name);
/// <inheritdoc/>
public byte Sex => ptr->Sex;
public byte Sex => this.Struct->Sex;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(ptr->ClassJob);
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
/// <inheritdoc/>
public byte Level => ptr->Level;
public byte Level => this.Struct->Level;
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();
}
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
}

View file

@ -1,49 +1,61 @@
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>
/// Interface representing a status.
/// This class represents a status effect an actor is afflicted by.
/// </summary>
public interface IStatus : IEquatable<IStatus>
public unsafe class Status
{
/// <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>
nint Address { get; }
public IntPtr Address { get; }
/// <summary>
/// Gets the status ID of this status.
/// </summary>
uint StatusId { get; }
public uint StatusId => this.Struct->StatusId;
/// <summary>
/// Gets the GameData associated with this status.
/// </summary>
RowRef<Lumina.Excel.Sheets.Status> GameData { get; }
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
/// <summary>
/// Gets the parameter value of the status.
/// </summary>
ushort Param { get; }
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;
/// <summary>
/// Gets the time remaining of this status.
/// </summary>
float RemainingTime { get; }
public float RemainingTime => this.Struct->RemainingTime;
/// <summary>
/// Gets the source ID of this status.
/// </summary>
uint SourceId { get; }
public uint SourceId => this.Struct->SourceObject.ObjectId;
/// <summary>
/// Gets the source actor associated with this status.
@ -51,55 +63,7 @@ public interface IStatus : IEquatable<IStatus>
/// <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);
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);
}
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
using Dalamud.Game.Player;
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(nint address)
internal StatusList(IntPtr address)
{
this.Address = address;
}
@ -26,19 +26,19 @@ public sealed unsafe partial class StatusList
/// </summary>
/// <param name="pointer">Pointer to the status list.</param>
internal unsafe StatusList(void* pointer)
: this((nint)pointer)
: this((IntPtr)pointer)
{
}
/// <summary>
/// Gets the address of the status list in memory.
/// </summary>
public nint Address { get; }
public IntPtr Address { get; }
/// <summary>
/// Gets the amount of status effect slots the actor has.
/// </summary>
public int Length => this.Struct->NumValidStatuses;
public int Length => Struct->NumValidStatuses;
private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>();
@ -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 IStatus? this[int index]
public Status? 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(nint address)
public static StatusList? CreateStatusListReference(IntPtr address)
{
if (address == IntPtr.Zero)
return null;
@ -74,12 +74,8 @@ 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 clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == 0)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new StatusList(address);
@ -90,15 +86,16 @@ 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 IStatus? CreateStatusReference(nint address)
public static Status? CreateStatusReference(IntPtr address)
{
if (address == IntPtr.Zero)
return null;
if (address == 0)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new Status((CSStatus*)address);
return new Status(address);
}
/// <summary>
@ -106,22 +103,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 nint GetStatusAddress(int index)
public IntPtr GetStatusAddress(int index)
{
if (index < 0 || index >= this.Length)
return 0;
return IntPtr.Zero;
return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]);
return (IntPtr)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<IStatus>, ICollection
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
{
/// <inheritdoc/>
int IReadOnlyCollection<IStatus>.Count => this.Length;
int IReadOnlyCollection<Status>.Count => this.Length;
/// <inheritdoc/>
int ICollection.Count => this.Length;
@ -133,9 +130,17 @@ public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollecti
object ICollection.SyncRoot => this;
/// <inheritdoc/>
public IEnumerator<IStatus> GetEnumerator()
public IEnumerator<Status> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
var status = this[i];
if (status == null || status.StatusId == 0)
continue;
yield return status;
}
}
/// <inheritdoc/>
@ -150,38 +155,4 @@ public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollecti
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()
{
}
}
}

View file

@ -0,0 +1,35 @@
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;
}

Some files were not shown because too many files have changed in this diff Show more