Merge branch 'master' into feature/sestring-to-texture

This commit is contained in:
goat 2025-12-04 00:57:07 +01:00 committed by GitHub
commit 3fbc24904a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
490 changed files with 8689 additions and 3741 deletions

211
.github/generate_changelog.py vendored Normal file
View file

@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""
Generate a changelog from git commits between the last two tags and post to Discord webhook.
"""
import subprocess
import re
import sys
import json
import argparse
from typing import List, Tuple, Optional
def run_git_command(args: List[str]) -> str:
"""Run a git command and return its output."""
try:
result = subprocess.run(
["git"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Git command failed: {e}", file=sys.stderr)
sys.exit(1)
def get_last_two_tags() -> Tuple[str, str]:
"""Get the latest two git tags."""
tags = run_git_command(["tag", "--sort=-version:refname"])
tag_list = [t for t in tags.split("\n") if t]
# Filter out old tags that start with 'v' (old versioning scheme)
tag_list = [t for t in tag_list if not t.startswith('v')]
if len(tag_list) < 2:
print("Error: Need at least 2 tags in the repository", file=sys.stderr)
sys.exit(1)
return tag_list[0], tag_list[1]
def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
"""Get the commit hash of a submodule at a specific tag."""
try:
# Get the submodule commit at the specified tag
result = run_git_command(["ls-tree", tag, submodule_path])
# Format is: "<mode> commit <hash>\t<path>"
parts = result.split()
if len(parts) >= 3 and parts[1] == "commit":
return parts[2]
return None
except:
return None
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]:
"""Get commits between two tags. Returns list of (message, author) tuples."""
log_output = run_git_command([
"log",
f"{tag2}..{tag1}",
"--format=%s|%an|%h"
])
commits = []
for line in log_output.split("\n"):
if "|" in line:
message, author, sha = line.split("|", 2)
commits.append((message.strip(), author.strip(), sha.strip()))
return commits
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]:
"""Filter out commits matching any of the ignore patterns."""
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
filtered = []
for message, author, sha in commits:
if not any(pattern.search(message) for pattern in compiled_patterns):
filtered.append((message, author, sha))
return filtered
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str:
"""Generate markdown changelog."""
# Calculate statistics
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 **{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"
changelog += f"[Click here](<https://github.com/aers/FFXIVClientStructs/compare/{cs_commit_old}...{cs_commit_new}>) to see all CS changes.\n"
elif cs_commit_new:
changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += "## Dalamud Changes\n\n"
for message, author, sha in commits:
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n"
return changelog
def post_to_discord(webhook_url: str, content: str, version: str) -> None:
"""Post changelog to Discord webhook as a file attachment."""
try:
import requests
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
filename = f"changelog-v{version}.md"
# Prepare the payload
data = {
"content": f"Dalamud v{version} has been released!",
"attachments": [
{
"id": "0",
"filename": filename
}
]
}
# Prepare the files
files = {
"payload_json": (None, json.dumps(data)),
"files[0]": (filename, content.encode('utf-8'), 'text/markdown')
}
try:
result = requests.post(webhook_url, files=files)
result.raise_for_status()
print(f"Successfully posted to Discord webhook, code {result.status_code}")
except requests.exceptions.HTTPError as err:
print(f"Failed to post to Discord: {err}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Failed to post to Discord: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Generate changelog from git commits and post to Discord webhook"
)
parser.add_argument(
"--webhook-url",
required=True,
help="Discord webhook URL"
)
parser.add_argument(
"--ignore",
action="append",
default=[],
help="Regex patterns to ignore commits (can be specified multiple times)"
)
parser.add_argument(
"--submodule-path",
default="lib/FFXIVClientStructs",
help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)"
)
args = parser.parse_args()
# Get the last two tags
latest_tag, previous_tag = get_last_two_tags()
print(f"Generating changelog between {previous_tag} and {latest_tag}")
# Get submodule commits at both tags
cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag)
cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag)
if cs_commit_new:
print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}")
if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
# Get commits between tags
commits = get_commits_between_tags(latest_tag, previous_tag)
print(f"Found {len(commits)} commits")
# 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_commits,
cs_commit_new, cs_commit_old)
print("\n" + "="*50)
print("Generated Changelog:")
print("="*50)
print(changelog)
print("="*50 + "\n")
# Post to Discord
post_to_discord(args.webhook_url, changelog, latest_tag)
if __name__ == "__main__":
main()

32
.github/workflows/backup.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Back up code to other forges
on:
schedule:
- cron: '0 2 * * *' # Run every day at 2 AM
workflow_dispatch: # Allow manual trigger
jobs:
push-to-forges:
runs-on: ubuntu-latest
if: github.repository == 'goatcorp/Dalamud'
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd #v0.9.1
with:
ssh-private-key: |
${{ secrets.MIRROR_GITLAB_SYNC_KEY }}
${{ secrets.MIRROR_CODEBERG_SYNC_KEY }}
- name: Add remotes & push
env:
GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=accept-new"
run: |
git remote add gitlab git@gitlab.com:goatcorp/Dalamud.git
git push gitlab --all --force
git remote add codeberg git@codeberg.org:goatcorp/Dalamud.git
git push codeberg --all --force

View file

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

View file

@ -1,8 +1,8 @@
name: Rollup changes to next version
on:
# push:
# branches:
# - master
push:
branches:
- master
workflow_dispatch:
jobs:
@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
branches:
- net9
- api14
defaults:
run:

View file

@ -1,16 +1,26 @@
name: Check for FFXIVCS changes
name: Check for Submodule Changes
on:
schedule:
- cron: "0 0,12,18 */1 * *"
- cron: "0 0,6,12,18 * * *"
workflow_dispatch:
jobs:
check:
name: FFXIVCS Check
name: Check ${{ matrix.submodule.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branches: [master]
submodule:
- name: ClientStructs
path: lib/FFXIVClientStructs
branch: main
branch-prefix: csupdate
- name: Excel Schema
path: lib/Lumina.Excel
branch: master
branch-prefix: schemaupdate
defaults:
run:
@ -24,30 +34,41 @@ jobs:
ref: ${{ matrix.branches }}
token: ${{ secrets.UPDATE_PAT }}
- name: Create update branch
run: git checkout -b csupdate/${{ matrix.branches }}
run: git checkout -b ${{ matrix.submodule.branch-prefix }}/${{ matrix.branches }}
- name: Initialize mandatory git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email noreply@github.com
git config --global pull.rebase false
- name: Update submodule
id: update-submodule
run: |
git checkout -b csupdate-${{ matrix.branches }}
git checkout -b ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }}
git reset --hard origin/${{ matrix.branches }}
cd lib/FFXIVClientStructs
cd ${{ matrix.submodule.path }}
git fetch
git reset --hard origin/main
git reset --hard origin/${{ matrix.submodule.branch }}
cd ../..
git add lib/FFXIVClientStructs
git commit --message "Update ClientStructs"
git push origin csupdate-${{ matrix.branches }} --force
git add ${{ matrix.submodule.path }}
if [[ -z "$(git status --porcelain --untracked-files=no)" ]]; then
echo "No changes detected!"
echo "SUBMIT_PR=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git commit --message "Update ${{ matrix.submodule.name }}"
git push origin ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --force
echo "SUBMIT_PR=true" >> "$GITHUB_OUTPUT"
- name: Create PR
if: ${{ steps.update-submodule.outputs.SUBMIT_PR == 'true' }}
run: |
echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token
prNumber=$(gh pr list --base ${{ matrix.branches }} --head csupdate-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}")
prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}")
if [ -z "$prNumber" ]; then
echo "No PR found, creating one"
gh pr create --head csupdate-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ClientStructs" --body "" --base ${{ matrix.branches }}
gh pr create --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ${{ matrix.submodule.name }}" --body "" --base ${{ matrix.branches }}
else
echo "PR already exists, ignoring"
fi

3
.gitmodules vendored
View file

@ -19,3 +19,6 @@
[submodule "lib/Hexa.NET.ImGui"]
path = lib/Hexa.NET.ImGui
url = https://github.com/goatcorp/Hexa.NET.ImGui.git
[submodule "lib/Lumina.Excel"]
path = lib/Lumina.Excel
url = https://github.com/NotAdam/Lumina.Excel.git

View file

@ -26,6 +26,38 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_APPNAME "Dalamud Boot"
IDS_MSVCRT_ACTION_OPENDOWNLOAD
"Download Microsoft Visual C++ Redistributable 2022\nExit the game and download the latest setup file from Microsoft."
IDS_MSVCRT_ACTION_IGNORE
"Ignore and Continue\nAttempt to continue with the currently installed version.\nDalamud or plugins may fail to load."
IDS_MSVCRT_DIALOG_MAININSTRUCTION
"Outdated Microsoft Visual C++ Redistributable"
IDS_MSVCRT_DIALOG_CONTENT
"The Microsoft Visual C++ Redistributable version detected on this computer (v{0}.{1}.{2}.{3}) is out of date and may not work with Dalamud."
IDS_MSVCRT_DOWNLOADURL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
IDS_INITIALIZEFAIL_ACTION_ABORT "Abort\nExit the game."
IDS_INITIALIZEFAIL_ACTION_CONTINUE
"Load game without Dalamud\nThe game will launch without Dalamud enabled."
IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION "Failed to load Dalamud."
IDS_INITIALIZEFAIL_DIALOG_CONTENT
"An error is preventing Dalamud from being loaded along with the game."
END
STRINGTABLE
BEGIN
IDS_INITIALIZEFAIL_DIALOG_FOOTER
"Last operation: {0}\nHRESULT: 0x{1:08X}\nDescription: {2}"
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////

View file

@ -48,7 +48,7 @@
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard>stdcpp23</LanguageStandard>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -65,7 +65,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
@ -80,7 +80,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">26812</DisableSpecificWarnings>
@ -133,6 +133,10 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="DalamudStartInfo.cpp" />
<ClCompile Include="error_info.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="hooks.cpp" />
<ClCompile Include="logging.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
@ -176,6 +180,7 @@
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\trampoline.h" />
<ClInclude Include="crashhandler_shared.h" />
<ClInclude Include="DalamudStartInfo.h" />
<ClInclude Include="error_info.h" />
<ClInclude Include="hooks.h" />
<ClInclude Include="logging.h" />
<ClInclude Include="ntdll.h" />
@ -206,4 +211,4 @@
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
</Target>
</Project>
</Project>

View file

@ -76,6 +76,9 @@
<ClCompile Include="ntdll.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="error_info.cpp">
<Filter>Common Boot</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
@ -146,6 +149,9 @@
<ClInclude Include="ntdll.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="error_info.h">
<Filter>Common Boot</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Dalamud.Boot.rc" />

View file

@ -9,10 +9,125 @@
#include "utils.h"
#include "veh.h"
#include "xivfixes.h"
#include "resource.h"
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
static void CheckMsvcrtVersion() {
// Commit introducing inline mutex ctor: tagged vs-2022-17.14 (2024-06-18)
// - https://github.com/microsoft/STL/commit/22a88260db4d754bbc067e2002430144d6ec5391
// MSVC Redist versions:
// - https://github.com/abbodi1406/vcredist/blob/master/source_links/README.md
// - 14.40.33810.0 dsig 2024-04-28
// - 14.40.33816.0 dsig 2024-09-11
constexpr WORD RequiredMsvcrtVersionComponents[] = {14, 40, 33816, 0};
constexpr auto RequiredMsvcrtVersion = 0ULL
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[0]) << 48)
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[1]) << 32)
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[2]) << 16)
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[3]) << 0);
constexpr const wchar_t* RuntimeDllNames[] = {
#ifdef _DEBUG
L"msvcp140d.dll",
L"vcruntime140d.dll",
L"vcruntime140_1d.dll",
#else
L"msvcp140.dll",
L"vcruntime140.dll",
L"vcruntime140_1.dll",
#endif
};
uint64_t lowestVersion = 0;
for (const auto& runtimeDllName : RuntimeDllNames) {
const utils::loaded_module mod(GetModuleHandleW(runtimeDllName));
if (!mod) {
logging::E("MSVCRT DLL not found: {}", runtimeDllName);
continue;
}
const auto path = mod.path()
.transform([](const auto& p) { return p.wstring(); })
.value_or(runtimeDllName);
if (const auto versionResult = mod.get_file_version()) {
const auto& versionFull = versionResult->get();
logging::I("MSVCRT DLL {} has version {}.", path, utils::format_file_version(versionFull));
const auto version = 0ULL |
(static_cast<uint64_t>(versionFull.dwFileVersionMS) << 32) |
(static_cast<uint64_t>(versionFull.dwFileVersionLS) << 0);
if (version < RequiredMsvcrtVersion && (lowestVersion == 0 || lowestVersion > version))
lowestVersion = version;
} else {
logging::E("Failed to detect MSVCRT DLL version for {}: {}", path, versionResult.error().describe());
}
}
if (!lowestVersion)
return;
enum IdTaskDialogAction {
IdTaskDialogActionOpenDownload = 101,
IdTaskDialogActionIgnore,
};
const TASKDIALOG_BUTTON buttons[]{
{IdTaskDialogActionOpenDownload, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_OPENDOWNLOAD)},
{IdTaskDialogActionIgnore, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_IGNORE)},
};
const WORD lowestVersionComponents[]{
static_cast<WORD>(lowestVersion >> 48),
static_cast<WORD>(lowestVersion >> 32),
static_cast<WORD>(lowestVersion >> 16),
static_cast<WORD>(lowestVersion >> 0),
};
const auto dialogContent = std::vformat(
utils::get_string_resource(IDS_MSVCRT_DIALOG_CONTENT),
std::make_wformat_args(
lowestVersionComponents[0],
lowestVersionComponents[1],
lowestVersionComponents[2],
lowestVersionComponents[3]));
const TASKDIALOGCONFIG config{
.cbSize = sizeof config,
.hInstance = g_hModule,
.dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS,
.pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
.pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
.pszMainInstruction = MAKEINTRESOURCEW(IDS_MSVCRT_DIALOG_MAININSTRUCTION),
.pszContent = dialogContent.c_str(),
.cButtons = _countof(buttons),
.pButtons = buttons,
.nDefaultButton = IdTaskDialogActionOpenDownload,
};
int buttonPressed;
if (utils::scoped_dpi_awareness_context ctx;
FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
buttonPressed = IdTaskDialogActionOpenDownload;
switch (buttonPressed) {
case IdTaskDialogActionOpenDownload:
ShellExecuteW(
nullptr,
L"open",
utils::get_string_resource(IDS_MSVCRT_DOWNLOADURL).c_str(),
nullptr,
nullptr,
SW_SHOW);
ExitProcess(0);
break;
}
}
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();
@ -24,7 +139,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
}
if (g_startInfo.BootShowConsole)
ConsoleSetup(L"Dalamud Boot");
ConsoleSetup(utils::get_string_resource(IDS_APPNAME).c_str());
logging::update_dll_load_status(true);
@ -94,6 +209,8 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize) != DalamudStartInfo::WaitMessageboxFlags::None)
MessageBoxW(nullptr, L"Press OK to continue (BeforeInitialize)", L"Dalamud Boot", MB_OK);
CheckMsvcrtVersion();
if (g_startInfo.BootDebugDirectX) {
logging::I("Enabling DirectX Debugging.");
@ -159,7 +276,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if (minHookLoaded) {
logging::I("Applying fixes...");
xivfixes::apply_all(true);
std::thread([] { xivfixes::apply_all(true); }).join();
logging::I("Fixes OK");
} else {
logging::W("Skipping fixes, as MinHook has failed to load.");
@ -170,11 +287,14 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
while (!IsDebuggerPresent())
Sleep(100);
logging::I("Debugger attached.");
__debugbreak();
}
const auto fs_module_path = utils::get_module_path(g_hModule);
const auto runtimeconfig_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
const auto module_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.dll").wstring();
const auto fs_module_path = utils::loaded_module(g_hModule).path();
if (!fs_module_path)
return fs_module_path.error();
const auto runtimeconfig_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
const auto module_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.dll").wstring();
// ============================== CLR ========================================= //

View file

@ -0,0 +1,26 @@
#include "error_info.h"
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept
: m_dalamudErrorDescription(dalamudErrorDescription)
, m_hresult(hresult) {
}
DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept
: DalamudBootError(dalamudErrorDescription, E_FAIL) {
}
const char* DalamudBootError::describe() const {
switch (m_dalamudErrorDescription) {
case DalamudBootErrorDescription::ModuleResourceLoadFail:
return "Failed to load resource.";
case DalamudBootErrorDescription::ModuleResourceVersionReadFail:
return "Failed to query version information.";
case DalamudBootErrorDescription::ModuleResourceVersionSignatureFail:
return "Invalid version info found.";
default:
return "(unavailable)";
}
}

42
Dalamud.Boot/error_info.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include <expected>
#include <string>
typedef unsigned long DWORD;
typedef _Return_type_success_(return >= 0) long HRESULT;
enum class DalamudBootErrorDescription {
None,
ModulePathResolutionFail,
ModuleResourceLoadFail,
ModuleResourceVersionReadFail,
ModuleResourceVersionSignatureFail,
};
class DalamudBootError {
DalamudBootErrorDescription m_dalamudErrorDescription;
long m_hresult;
public:
DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept;
DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept;
const char* describe() const;
operator HRESULT() const {
return m_hresult;
}
};
template<typename T>
using DalamudExpected = std::expected<
std::conditional_t<
std::is_reference_v<T>,
std::reference_wrapper<std::remove_reference_t<T>>,
T
>,
DalamudBootError
>;
using DalamudUnexpected = std::unexpected<DalamudBootError>;

View file

@ -84,19 +84,13 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
const auto dllName = unicode::convert<std::string>(pData->Loaded.FullDllName->Buffer);
utils::loaded_module mod(pData->Loaded.DllBase);
std::wstring version, description;
try {
version = utils::format_file_version(mod.get_file_version());
} catch (...) {
version = L"<unknown>";
}
try {
description = mod.get_description();
} catch (...) {
description = L"<unknown>";
}
const auto version = mod.get_file_version()
.transform([](const auto& v) { return utils::format_file_version(v.get()); })
.value_or(L"<unknown>");
const auto description = mod.get_description()
.value_or(L"<unknown>");
logging::I(R"({} "{}" ("{}" ver {}) has been loaded at 0x{:X} ~ 0x{:X} (0x{:X}); finding import table items to hook.)",
LogTag, dllName, description, version,
reinterpret_cast<size_t>(pData->Loaded.DllBase),
@ -125,7 +119,9 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
if (mod.is_current_process())
return;
const auto path = unicode::convert<std::string>(mod.path().wstring());
const auto path = mod.path()
.transform([](const auto& p) { return unicode::convert<std::string>(p.wstring()); })
.value_or("<unknown>");
for (const auto& [hModule, targetFns] : m_targetFns) {
for (const auto& [targetFn, pfnThunk] : targetFns) {
@ -133,7 +129,7 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
if (void* pGetProcAddressImport; mod.find_imported_function_pointer(dllName.c_str(), targetFn.c_str(), 0, pGetProcAddressImport)) {
auto& hook = m_hooks[hModule][targetFn][mod];
if (!hook) {
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, unicode::convert<std::string>(mod.path().wstring()));
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, path);
hook.emplace(std::format("getprocaddress_singleton_import_hook::hook_module({}!{})", dllName, targetFn), static_cast<void**>(pGetProcAddressImport), pfnThunk);
}

View file

@ -11,6 +11,9 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
// https://developercommunity.visualstudio.com/t/Access-violation-with-std::mutex::lock-a/10664660
#define _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR
// Windows Header Files (1)
#include <Windows.h>
@ -21,6 +24,7 @@
#include <iphlpapi.h>
#include <PathCch.h>
#include <Psapi.h>
#include <shellapi.h>
#include <ShlObj.h>
#include <Shlwapi.h>
#include <SubAuth.h>
@ -51,6 +55,7 @@
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
// https://www.akenotsuki.com/misc/srell/en/

View file

@ -3,12 +3,23 @@
// Used by Dalamud.Boot.rc
//
#define IDI_ICON1 101
#define IDS_APPNAME 102
#define IDS_MSVCRT_ACTION_OPENDOWNLOAD 103
#define IDS_MSVCRT_ACTION_IGNORE 104
#define IDS_MSVCRT_DIALOG_MAININSTRUCTION 105
#define IDS_MSVCRT_DIALOG_CONTENT 106
#define IDS_MSVCRT_DOWNLOADURL 107
#define IDS_INITIALIZEFAIL_ACTION_ABORT 108
#define IDS_INITIALIZEFAIL_ACTION_CONTINUE 109
#define IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION 110
#define IDS_INITIALIZEFAIL_DIALOG_CONTENT 111
#define IDS_INITIALIZEFAIL_DIALOG_FOOTER 112
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101

View file

@ -2,6 +2,7 @@
#include "logging.h"
#include "utils.h"
#include "resource.h"
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
@ -379,12 +380,50 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara
auto desc = err.Description();
if (desc.length() == 0)
desc = err.ErrorMessage();
if (MessageBoxW(nullptr, std::format(
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
last_operation,
desc.GetBSTR()).c_str(),
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
ExitProcess(-1);
enum IdTaskDialogAction {
IdTaskDialogActionAbort = 101,
IdTaskDialogActionContinue,
};
const TASKDIALOG_BUTTON buttons[]{
{IdTaskDialogActionAbort, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_ABORT)},
{IdTaskDialogActionContinue, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_CONTINUE)},
};
const auto hru32 = static_cast<uint32_t>(hr);
const auto footer = std::vformat(
utils::get_string_resource(IDS_INITIALIZEFAIL_DIALOG_FOOTER),
std::make_wformat_args(
last_operation,
hru32,
desc.GetBSTR()));
const TASKDIALOGCONFIG config{
.cbSize = sizeof config,
.hInstance = g_hModule,
.dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_EXPAND_FOOTER_AREA,
.pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
.pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
.pszMainInstruction = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION),
.pszContent = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_CONTENT),
.cButtons = _countof(buttons),
.pButtons = buttons,
.nDefaultButton = IdTaskDialogActionAbort,
.pszFooter = footer.c_str(),
};
int buttonPressed;
if (utils::scoped_dpi_awareness_context ctx;
FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
buttonPressed = IdTaskDialogActionAbort;
switch (buttonPressed) {
case IdTaskDialogActionAbort:
ExitProcess(-1);
break;
}
if (hMainThreadContinue) {
CloseHandle(hMainThreadContinue);
hMainThreadContinue = nullptr;

View file

@ -3,22 +3,27 @@
#include "utils.h"
std::filesystem::path utils::loaded_module::path() const {
std::wstring buf(MAX_PATH, L'\0');
for (;;) {
if (const auto len = GetModuleFileNameExW(GetCurrentProcess(), m_hModule, &buf[0], static_cast<DWORD>(buf.size())); len != buf.size()) {
if (buf.empty())
throw std::runtime_error(std::format("Failed to resolve module path: Win32 error {}", GetLastError()));
DalamudExpected<std::filesystem::path> utils::loaded_module::path() const {
for (std::wstring buf(MAX_PATH, L'\0');; buf.resize(buf.size() * 2)) {
if (const auto len = GetModuleFileNameW(m_hModule, &buf[0], static_cast<DWORD>(buf.size()));
len != buf.size()) {
if (!len) {
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModulePathResolutionFail,
HRESULT_FROM_WIN32(GetLastError()));
}
buf.resize(len);
return buf;
}
if (buf.size() * 2 < PATHCCH_MAX_CCH)
buf.resize(buf.size() * 2);
else if (auto p = std::filesystem::path(buf); exists(p))
return p;
else
throw std::runtime_error("Failed to resolve module path: no amount of buffer size would fit the data");
if (buf.size() > PATHCCH_MAX_CCH) {
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModulePathResolutionFail,
E_OUTOFMEMORY);
}
}
}
@ -144,66 +149,90 @@ void* utils::loaded_module::get_imported_function_pointer(const char* pcszDllNam
throw std::runtime_error(std::format("Failed to find import for {}!{} ({}).", pcszDllName, pcszFunctionName ? pcszFunctionName : "<unnamed>", hintOrOrdinal));
}
std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
DalamudExpected<std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
const auto hres = FindResourceW(m_hModule, lpName, lpType);
if (!hres)
throw std::runtime_error("No such resource");
return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
const auto hRes = LoadResource(m_hModule, hres);
if (!hRes)
throw std::runtime_error("LoadResource failure");
return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
return {hRes, &FreeResource};
return std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>(hRes, &FreeResource);
}
std::wstring utils::loaded_module::get_description() const {
const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
const auto pBlock = LockResource(rsrc.get());
DalamudExpected<std::wstring> utils::loaded_module::get_description() const {
auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
if (!rsrc)
return DalamudUnexpected(std::move(rsrc.error()));
const auto pBlock = LockResource(rsrc->get());
struct LANGANDCODEPAGE {
WORD wLanguage;
WORD wCodePage;
} * lpTranslate;
UINT cbTranslate;
if (!VerQueryValueW(pBlock,
TEXT("\\VarFileInfo\\Translation"),
L"\\VarFileInfo\\Translation",
reinterpret_cast<LPVOID*>(&lpTranslate),
&cbTranslate)) {
throw std::runtime_error("Invalid version information (1)");
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
HRESULT_FROM_WIN32(GetLastError()));
}
for (size_t i = 0; i < (cbTranslate / sizeof(LANGANDCODEPAGE)); i++) {
wchar_t subblockNameBuf[64];
*std::format_to_n(
subblockNameBuf,
_countof(subblockNameBuf),
L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage).out = 0;;
wchar_t* buf = nullptr;
UINT size = 0;
if (!VerQueryValueW(pBlock,
std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage).c_str(),
reinterpret_cast<LPVOID*>(&buf),
&size)) {
if (!VerQueryValueW(pBlock, subblockNameBuf, reinterpret_cast<LPVOID*>(&buf), &size))
continue;
}
auto currName = std::wstring_view(buf, size);
while (!currName.empty() && currName.back() == L'\0')
currName = currName.substr(0, currName.size() - 1);
if (const auto p = currName.find(L'\0'); p != std::string::npos)
currName = currName.substr(0, p);
if (currName.empty())
continue;
return std::wstring(currName);
}
throw std::runtime_error("Invalid version information (2)");
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
}
VS_FIXEDFILEINFO utils::loaded_module::get_file_version() const {
const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
const auto pBlock = LockResource(rsrc.get());
std::expected<std::reference_wrapper<const VS_FIXEDFILEINFO>, DalamudBootError> utils::loaded_module::get_file_version() const {
auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
if (!rsrc)
return DalamudUnexpected(std::move(rsrc.error()));
const auto pBlock = LockResource(rsrc->get());
UINT size = 0;
LPVOID lpBuffer = nullptr;
if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size))
throw std::runtime_error("Failed to query version information.");
if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size)) {
return std::unexpected<DalamudBootError>(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
HRESULT_FROM_WIN32(GetLastError()));
}
const VS_FIXEDFILEINFO& versionInfo = *static_cast<const VS_FIXEDFILEINFO*>(lpBuffer);
if (versionInfo.dwSignature != 0xfeef04bd)
throw std::runtime_error("Invalid version info found.");
if (versionInfo.dwSignature != 0xfeef04bd) {
return std::unexpected<DalamudBootError>(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionSignatureFail);
}
return versionInfo;
}
@ -353,7 +382,7 @@ const char* utils::signature_finder::result::resolve_jump_target(size_t instruct
nmd_x86_instruction instruction{};
if (!nmd_x86_decode(&Match[instructionOffset], NMD_X86_MAXIMUM_INSTRUCTION_LENGTH, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL))
throw std::runtime_error("Matched address does not have a valid assembly instruction");
size_t numExplicitOperands = 0;
for (size_t i = 0; i < instruction.num_operands; i++)
numExplicitOperands += instruction.operands[i].is_implicit ? 0 : 1;
@ -589,17 +618,10 @@ bool utils::is_running_on_wine() {
return g_startInfo.Platform != "WINDOWS";
}
std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0');
while (true) {
if (const auto res = GetModuleFileNameW(hModule, &buf[0], static_cast<int>(buf.size())); !res)
throw std::runtime_error(std::format("GetModuleFileName failure: 0x{:X}", GetLastError()));
else if (res < buf.size()) {
buf.resize(res);
return buf;
} else
buf.resize(buf.size() * 2);
}
std::wstring utils::get_string_resource(uint32_t resId) {
LPCWSTR pstr;
const auto len = LoadStringW(g_hModule, resId, reinterpret_cast<LPWSTR>(&pstr), 0);
return std::wstring(pstr, len);
}
HWND utils::try_find_game_window() {
@ -625,7 +647,7 @@ void utils::wait_for_game_window() {
std::wstring utils::escape_shell_arg(const std::wstring& arg) {
// https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
std::wstring res;
if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) {
res.append(arg);
@ -677,3 +699,22 @@ std::wstring utils::format_win32_error(DWORD err) {
return std::format(L"Win32 error ({}=0x{:X})", err, err);
}
utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context()
: scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
}
utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT context) {
const auto user32 = GetModuleHandleW(L"user32.dll");
m_setThreadDpiAwarenessContext =
user32
? reinterpret_cast<decltype(&SetThreadDpiAwarenessContext)>(
GetProcAddress(user32, "SetThreadDpiAwarenessContext"))
: nullptr;
m_old = m_setThreadDpiAwarenessContext ? m_setThreadDpiAwarenessContext(context) : DPI_AWARENESS_CONTEXT_UNAWARE;
}
utils::scoped_dpi_awareness_context::~scoped_dpi_awareness_context() {
if (m_setThreadDpiAwarenessContext)
m_setThreadDpiAwarenessContext(m_old);
}

View file

@ -1,5 +1,6 @@
#pragma once
#include <expected>
#include <filesystem>
#include <functional>
#include <span>
@ -7,6 +8,7 @@
#include <memory>
#include <vector>
#include "error_info.h"
#include "unicode.h"
namespace utils {
@ -18,14 +20,13 @@ namespace utils {
loaded_module(void* hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
loaded_module(size_t hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
std::filesystem::path path() const;
DalamudExpected<std::filesystem::path> path() const;
bool is_current_process() const { return m_hModule == GetModuleHandleW(nullptr); }
bool owns_address(const void* pAddress) const;
operator HMODULE() const {
return m_hModule;
}
operator HMODULE() const { return m_hModule; }
operator bool() const { return m_hModule; }
size_t address_int() const { return reinterpret_cast<size_t>(m_hModule); }
size_t image_size() const { return is_pe64() ? nt_header64().OptionalHeader.SizeOfImage : nt_header32().OptionalHeader.SizeOfImage; }
@ -58,9 +59,9 @@ namespace utils {
void* get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) const;
template<typename TFn> TFn** get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) { return reinterpret_cast<TFn**>(get_imported_function_pointer(pcszDllName, pcszFunctionName, hintOrOrdinal)); }
[[nodiscard]] std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
[[nodiscard]] std::wstring get_description() const;
[[nodiscard]] VS_FIXEDFILEINFO get_file_version() const;
[[nodiscard]] DalamudExpected<std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
[[nodiscard]] DalamudExpected<std::wstring> get_description() const;
[[nodiscard]] DalamudExpected<const VS_FIXEDFILEINFO&> get_file_version() const;
static loaded_module current_process();
static std::vector<loaded_module> all_modules();
@ -269,7 +270,7 @@ namespace utils {
bool is_running_on_wine();
std::filesystem::path get_module_path(HMODULE hModule);
std::wstring get_string_resource(uint32_t resId);
/// @brief Find the game main window.
/// @return Handle to the game main window, or nullptr if it doesn't exist (yet).
@ -280,4 +281,18 @@ namespace utils {
std::wstring escape_shell_arg(const std::wstring& arg);
std::wstring format_win32_error(DWORD err);
class scoped_dpi_awareness_context {
DPI_AWARENESS_CONTEXT m_old;
decltype(&SetThreadDpiAwarenessContext) m_setThreadDpiAwarenessContext;
public:
scoped_dpi_awareness_context();
scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT);
~scoped_dpi_awareness_context();
scoped_dpi_awareness_context(const scoped_dpi_awareness_context&) = delete;
scoped_dpi_awareness_context(scoped_dpi_awareness_context&&) = delete;
scoped_dpi_awareness_context& operator=(const scoped_dpi_awareness_context&) = delete;
scoped_dpi_awareness_context& operator=(scoped_dpi_awareness_context&&) = delete;
};
}

View file

@ -102,9 +102,13 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address)
return false;
}
static void append_injector_launch_args(std::vector<std::wstring>& args)
static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstring>& args)
{
args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\"");
if (auto path = utils::loaded_module::current_process().path())
args.emplace_back(L"--game=\"" + path->wstring() + L"\"");
else
return DalamudUnexpected(std::in_place, std::move(path.error()));
switch (g_startInfo.DalamudLoadMethod) {
case DalamudStartInfo::LoadMethod::Entrypoint:
args.emplace_back(L"--mode=entrypoint");
@ -155,6 +159,8 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
args.emplace_back(szArgList[i]);
LocalFree(szArgList);
}
return {};
}
LONG exception_handler(EXCEPTION_POINTERS* ex)
@ -358,11 +364,20 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
args.emplace_back(std::format(L"--process-handle={}", reinterpret_cast<size_t>(hInheritableCurrentProcess)));
args.emplace_back(std::format(L"--exception-info-pipe-read-handle={}", reinterpret_cast<size_t>(hReadPipeInheritable->get())));
args.emplace_back(std::format(L"--asset-directory={}", unicode::convert<std::wstring>(g_startInfo.AssetDirectory)));
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
? utils::loaded_module(g_hModule).path().parent_path().wstring()
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
if (const auto path = utils::loaded_module(g_hModule).path()) {
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
? path->parent_path().wstring()
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
} else {
logging::W("Failed to read path of the Dalamud Boot module: {}", path.error().describe());
return false;
}
args.emplace_back(L"--");
append_injector_launch_args(args);
if (auto r = append_injector_launch_args(args); !r) {
logging::W("Failed to generate injector launch args: {}", r.error().describe());
return false;
}
for (const auto& arg : args)
{

View file

@ -8,12 +8,6 @@
#include "ntdll.h"
#include "utils.h"
template<typename T>
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
if (t.empty())
throw std::runtime_error(std::format("Unexpected empty span found: {}", descr));
return t;
}
void xivfixes::unhook_dll(bool bApply) {
static const auto LogTag = "[xivfixes:unhook_dll]";
static const auto LogTagW = L"[xivfixes:unhook_dll]";
@ -23,77 +17,90 @@ void xivfixes::unhook_dll(bool bApply) {
const auto mods = utils::loaded_module::all_modules();
const auto test_module = [&](size_t i, const utils::loaded_module & mod) {
std::filesystem::path path;
try {
path = mod.path();
std::wstring version, description;
try {
version = utils::format_file_version(mod.get_file_version());
} catch (...) {
version = L"<unknown>";
}
try {
description = mod.get_description();
} catch (...) {
description = L"<unknown>";
}
logging::I(R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))", LogTagW, i + 1, mods.size(), mod.address_int(), mod.address_int() + mod.image_size(), mod.image_size(), path.wstring(), description, version);
} catch (const std::exception& e) {
logging::W("{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}", LogTag, i + 1, mods.size(), mod.address_int(), e.what());
for (size_t i = 0; i < mods.size(); i++) {
const auto& mod = mods[i];
const auto path = mod.path();
if (!path) {
logging::W(
"{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}",
LogTag,
i + 1,
mods.size(),
mod.address_int(),
path.error().describe());
return;
}
const auto moduleName = unicode::convert<std::string>(path.filename().wstring());
const auto version = mod.get_file_version()
.transform([](const auto& v) { return utils::format_file_version(v.get()); })
.value_or(L"<unknown>");
std::vector<char> buf;
std::string formatBuf;
const auto description = mod.get_description()
.value_or(L"<unknown>");
logging::I(
R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))",
LogTagW,
i + 1,
mods.size(),
mod.address_int(),
mod.address_int() + mod.image_size(),
mod.image_size(),
path->wstring(),
description,
version);
const auto moduleName = unicode::convert<std::string>(path->filename().wstring());
const auto& sectionHeader = mod.section_header(".text");
const auto section = mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize);
if (section.empty()) {
logging::W("{} Error: .text[VA:VA + VS] is empty", LogTag);
return;
}
auto hFsDllRaw = CreateFileW(path->c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
return;
}
auto hFsDll = std::unique_ptr<void, decltype(&CloseHandle)>(hFsDllRaw, &CloseHandle);
std::vector<char> buf(section.size());
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
if (read < section.size_bytes()) {
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
return;
}
} else {
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
return;
}
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path->filename().u8string()));
try {
const auto& sectionHeader = mod.section_header(".text");
const auto section = assume_nonempty_span(mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize), ".text[VA:VA+VS]");
auto hFsDllRaw = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
return;
}
auto hFsDll = std::unique_ptr<void, decltype(CloseHandle)*>(hFsDllRaw, &CloseHandle);
buf.resize(section.size());
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
if (read < section.size_bytes()) {
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
return;
}
} else {
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
return;
}
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path.filename().u8string()));
std::optional<utils::memory_tenderizer> tenderizer;
for (size_t i = 0, instructionLength = 1, printed = 0; i < buf.size(); i += instructionLength) {
if (section[i] == buf[i]) {
std::string formatBuf;
for (size_t inst = 0, instructionLength = 1, printed = 0; inst < buf.size(); inst += instructionLength) {
if (section[inst] == buf[inst]) {
instructionLength = 1;
continue;
}
const auto rva = sectionHeader.VirtualAddress + i;
const auto rva = sectionHeader.VirtualAddress + inst;
nmd_x86_instruction instruction{};
if (!nmd_x86_decode(&section[i], section.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
if (!nmd_x86_decode(&section[inst], section.size() - inst, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
instructionLength = 1;
if (printed < 64) {
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[i]));
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[inst]));
printed++;
}
} else {
instructionLength = instruction.length;
if (printed < 64) {
formatBuf.resize(128);
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(&section[i]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(&section[inst]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
formatBuf.resize(strnlen(&formatBuf[0], formatBuf.size()));
const auto& directory = mod.data_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
@ -103,25 +110,25 @@ void xivfixes::unhook_dll(bool bApply) {
const auto functions = mod.span_as<DWORD>(exportDirectory.AddressOfFunctions, exportDirectory.NumberOfFunctions);
std::string resolvedExportName;
for (size_t j = 0; j < names.size(); ++j) {
for (size_t nameIndex = 0; nameIndex < names.size(); ++nameIndex) {
std::string_view name;
if (const char* pcszName = mod.address_as<char>(names[j]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
if (const char* pcszName = mod.address_as<char>(names[nameIndex]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
if (IsBadReadPtr(pcszName, 256)) {
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, j);
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, nameIndex);
continue;
}
name = std::string_view(pcszName, strnlen(pcszName, 256));
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, j, name);
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, nameIndex, name);
}
if (ordinals[j] >= functions.size()) {
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, j, ordinals[j], functions.size());
if (ordinals[nameIndex] >= functions.size()) {
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, nameIndex, ordinals[nameIndex], functions.size());
continue;
}
const auto rva = functions[ordinals[j]];
if (rva == &section[i] - mod.address()) {
const auto rva = functions[ordinals[nameIndex]];
if (rva == &section[inst] - mod.address()) {
resolvedExportName = std::format("[export:{}]", name);
break;
}
@ -135,7 +142,7 @@ void xivfixes::unhook_dll(bool bApply) {
if (doRestore) {
if (!tenderizer)
tenderizer.emplace(section, PAGE_EXECUTE_READWRITE);
memcpy(&section[i], &buf[i], instructionLength);
memcpy(&section[inst], &buf[inst], instructionLength);
}
}
@ -147,21 +154,7 @@ void xivfixes::unhook_dll(bool bApply) {
} catch (const std::exception& e) {
logging::W("{} Error: {}", LogTag, e.what());
}
};
// This is needed since try and __try cannot be used in the same function. Lambdas circumvent the limitation.
const auto windows_exception_handler = [&]() {
for (size_t i = 0; i < mods.size(); i++) {
const auto& mod = mods[i];
__try {
test_module(i, mod);
} __except (EXCEPTION_EXECUTE_HANDLER) {
logging::W("{} Error: Access Violation", LogTag);
}
}
};
windows_exception_handler();
}
}
using TFnGetInputDeviceManager = void* ();
@ -294,13 +287,11 @@ static bool is_xivalex(const std::filesystem::path& dllPath) {
static bool is_openprocess_already_dealt_with() {
static const auto s_value = [] {
for (const auto& mod : utils::loaded_module::all_modules()) {
try {
if (is_xivalex(mod.path()))
return true;
} catch (...) {
// pass
}
const auto path = mod.path().value_or({});
if (path.empty())
continue;
if (is_xivalex(path))
return true;
}
return false;
}();
@ -650,43 +641,22 @@ void xivfixes::symbol_load_patches(bool bApply) {
void xivfixes::disable_game_debugging_protection(bool bApply) {
static const char* LogTag = "[xivfixes:disable_game_debugging_protection]";
static const std::vector<uint8_t> patchBytes = {
0x31, 0xC0, // XOR EAX, EAX
0x90, // NOP
0x90, // NOP
0x90, // NOP
0x90 // NOP
};
static std::optional<hooks::import_hook<decltype(IsDebuggerPresent)>> s_hookIsDebuggerPresent;
if (!bApply)
return;
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
// Find IsDebuggerPresent in Framework.Tick()
const char* matchPtr = utils::signature_finder()
.look_in(utils::loaded_module(g_hGameInstance), ".text")
.look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41")
.find_one()
.Match.data();
if (!matchPtr) {
logging::E("{} Failed to find signature.", LogTag);
return;
}
void* address = const_cast<void*>(static_cast<const void*>(matchPtr));
DWORD oldProtect;
if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) {
memcpy(address, patchBytes.data(), patchBytes.size());
VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect);
logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast<uintptr_t>(address));
s_hookIsDebuggerPresent.emplace("kernel32.dll!IsDebuggerPresent", "kernel32.dll", "IsDebuggerPresent", 0);
s_hookIsDebuggerPresent->set_detour([]() { return false; });
logging::I("{} Enable", LogTag);
} else {
logging::E("{} Failed to change memory protection.", LogTag);
if (s_hookIsDebuggerPresent) {
logging::I("{} Disable", LogTag);
s_hookIsDebuggerPresent.reset();
}
}
}

View file

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>

View file

@ -25,10 +25,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PackageReference Include="Lumina" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -38,7 +38,7 @@
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard>stdcpp23</LanguageStandard>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -55,7 +55,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
@ -67,7 +67,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
@ -108,4 +108,4 @@
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
</Target>
</Project>
</Project>

View file

@ -52,17 +52,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Iced" Version="1.17.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="PeNet" Version="2.6.4" />
<PackageReference Include="Reloaded.Memory" Version="7.0.0" />
<PackageReference Include="Reloaded.Memory.Buffers" Version="2.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PackageReference Include="Iced" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.Windows.CsWin32" />
<PackageReference Include="PeNet" />
<PackageReference Include="Reloaded.Memory" />
<PackageReference Include="Reloaded.Memory.Buffers" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -12,14 +12,13 @@ using System.Text.RegularExpressions;
using Dalamud.Common;
using Dalamud.Common.Game;
using Dalamud.Common.Util;
using Newtonsoft.Json;
using Reloaded.Memory.Buffers;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using static Dalamud.Injector.NativeFunctions;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud.Injector
{
@ -268,9 +267,9 @@ namespace Dalamud.Injector
private static OSPlatform DetectPlatformHeuristic()
{
var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll");
var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call");
var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version");
var ntdll = Windows.Win32.PInvoke.GetModuleHandle("ntdll.dll");
var wineServerCallPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_server_call");
var wineGetHostVersionPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_get_host_version");
var winePlatform = GetWinePlatform(wineGetHostVersionPtr);
var isWine = wineServerCallPtr != nint.Zero;
@ -621,10 +620,13 @@ namespace Dalamud.Injector
if (warnManualInjection)
{
var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel);
var result = Windows.Win32.PInvoke.MessageBox(
HWND.Null,
$"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.",
"Dalamud",
MESSAGEBOX_STYLE.MB_ICONWARNING | MESSAGEBOX_STYLE.MB_OKCANCEL);
// IDCANCEL
if (result == 2)
if (result == MESSAGEBOX_RESULT.IDCANCEL)
{
Log.Information("User cancelled injection");
return -2;
@ -934,30 +936,48 @@ namespace Dalamud.Injector
Inject(process, startInfo, false);
}
var processHandleForOwner = IntPtr.Zero;
var processHandleForOwner = HANDLE.Null;
if (handleOwner != IntPtr.Zero)
{
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess))
unsafe
{
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
if (!Windows.Win32.PInvoke.DuplicateHandle(
new HANDLE(Process.GetCurrentProcess().Handle.ToPointer()),
new HANDLE(process.Handle.ToPointer()),
new HANDLE(handleOwner),
&processHandleForOwner,
0,
false,
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
{
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
}
}
}
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}");
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {(IntPtr)processHandleForOwner}}}");
Log.CloseAndFlush();
return 0;
}
private static Process GetInheritableCurrentProcessHandle()
private static unsafe Process GetInheritableCurrentProcessHandle()
{
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess))
var currentProcessHandle = new HANDLE(Process.GetCurrentProcess().Handle.ToPointer());
var inheritableHandle = HANDLE.Null;
if (!Windows.Win32.PInvoke.DuplicateHandle(
currentProcessHandle,
currentProcessHandle,
currentProcessHandle,
&inheritableHandle,
0,
true,
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
{
Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
return null;
throw new Win32Exception("Failed to call DuplicateHandle");
}
return new ExistingProcess(inheritableCurrentProcessHandle);
return new ExistingProcess(inheritableHandle);
}
private static int ProcessLaunchTestCommand(List<string> args)
@ -1048,13 +1068,13 @@ namespace Dalamud.Injector
}
injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);
injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode);
var exitCode = injector.CallRemoteFunction(initAddress, startInfoAddress);
// ======================================================
if (exitCode > 0)
{
Log.Error($"Dalamud.Boot::Initialize returned {exitCode}");
Log.Error("Dalamud.Boot::Initialize returned {ExitCode}", exitCode);
return;
}

View file

@ -13,8 +13,8 @@ using Reloaded.Memory.Buffers;
using Reloaded.Memory.Sources;
using Reloaded.Memory.Utilities;
using Serilog;
using Windows.Win32.Foundation;
using static Dalamud.Injector.NativeFunctions;
using static Iced.Intel.AssemblerRegisters;
namespace Dalamud.Injector
@ -88,7 +88,7 @@ namespace Dalamud.Injector
if (lpParameter == 0)
throw new Exception("Unable to allocate LoadLibraryW parameter");
this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter, out var err);
var err = this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter);
this.extMemory.Read<IntPtr>(this.loadLibraryRetPtr, out address);
if (address == IntPtr.Zero)
throw new Exception($"LoadLibraryW(\"{modulePath}\") failure: {new Win32Exception((int)err).Message} ({err})");
@ -108,7 +108,7 @@ namespace Dalamud.Injector
if (lpParameter == 0)
throw new Exception("Unable to allocate GetProcAddress parameter ptr");
this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter, out var err);
var err = this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter);
this.extMemory.Read<nuint>(this.getProcAddressRetPtr, out address);
if (address == 0)
throw new Exception($"GetProcAddress(0x{module:X}, \"{functionName}\") failure: {new Win32Exception((int)err).Message} ({err})");
@ -119,27 +119,30 @@ namespace Dalamud.Injector
/// </summary>
/// <param name="methodAddress">Method address.</param>
/// <param name="parameterAddress">Parameter address.</param>
/// <param name="exitCode">Thread exit code.</param>
public void CallRemoteFunction(nuint methodAddress, nuint parameterAddress, out uint exitCode)
/// <returns>Thread exit code.</returns>
public unsafe uint CallRemoteFunction(nuint methodAddress, nuint parameterAddress)
{
// Create and initialize a thread at our address and parameter address.
var threadHandle = CreateRemoteThread(
this.targetProcess.Handle,
IntPtr.Zero,
var threadHandle = Windows.Win32.PInvoke.CreateRemoteThread(
new HANDLE(this.targetProcess.Handle.ToPointer()),
null,
UIntPtr.Zero,
methodAddress,
parameterAddress,
CreateThreadFlags.RunImmediately,
out _);
(delegate* unmanaged[Stdcall]<void*, uint>)methodAddress,
parameterAddress.ToPointer(),
0, // Run immediately
null);
if (threadHandle == IntPtr.Zero)
throw new Exception($"CreateRemoteThread failure: {Marshal.GetLastWin32Error()}");
_ = WaitForSingleObject(threadHandle, uint.MaxValue);
_ = Windows.Win32.PInvoke.WaitForSingleObject(threadHandle, uint.MaxValue);
GetExitCodeThread(threadHandle, out exitCode);
uint exitCode = 0;
if (!Windows.Win32.PInvoke.GetExitCodeThread(threadHandle, &exitCode))
throw new Exception($"GetExitCodeThread failure: {Marshal.GetLastWin32Error()}");
CloseHandle(threadHandle);
Windows.Win32.PInvoke.CloseHandle(threadHandle);
return exitCode;
}
private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)

View file

@ -1,956 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Dalamud.Injector
{
/// <summary>
/// Native user32 functions.
/// </summary>
internal static partial class NativeFunctions
{
/// <summary>
/// MB_* from winuser.
/// </summary>
public enum MessageBoxType : uint
{
/// <summary>
/// The default value for any of the various subtypes.
/// </summary>
DefaultValue = 0x0,
// To indicate the buttons displayed in the message box, specify one of the following values.
/// <summary>
/// The message box contains three push buttons: Abort, Retry, and Ignore.
/// </summary>
AbortRetryIgnore = 0x2,
/// <summary>
/// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead
/// of MB_ABORTRETRYIGNORE.
/// </summary>
CancelTryContinue = 0x6,
/// <summary>
/// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends
/// a WM_HELP message to the owner.
/// </summary>
Help = 0x4000,
/// <summary>
/// The message box contains one push button: OK. This is the default.
/// </summary>
Ok = DefaultValue,
/// <summary>
/// The message box contains two push buttons: OK and Cancel.
/// </summary>
OkCancel = 0x1,
/// <summary>
/// The message box contains two push buttons: Retry and Cancel.
/// </summary>
RetryCancel = 0x5,
/// <summary>
/// The message box contains two push buttons: Yes and No.
/// </summary>
YesNo = 0x4,
/// <summary>
/// The message box contains three push buttons: Yes, No, and Cancel.
/// </summary>
YesNoCancel = 0x3,
// To display an icon in the message box, specify one of the following values.
/// <summary>
/// An exclamation-point icon appears in the message box.
/// </summary>
IconExclamation = 0x30,
/// <summary>
/// An exclamation-point icon appears in the message box.
/// </summary>
IconWarning = IconExclamation,
/// <summary>
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
/// </summary>
IconInformation = 0x40,
/// <summary>
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
/// </summary>
IconAsterisk = IconInformation,
/// <summary>
/// A question-mark icon appears in the message box.
/// The question-mark message icon is no longer recommended because it does not clearly represent a specific type
/// of message and because the phrasing of a message as a question could apply to any message type. In addition,
/// users can confuse the message symbol question mark with Help information. Therefore, do not use this question
/// mark message symbol in your message boxes. The system continues to support its inclusion only for backward
/// compatibility.
/// </summary>
IconQuestion = 0x20,
/// <summary>
/// A stop-sign icon appears in the message box.
/// </summary>
IconStop = 0x10,
/// <summary>
/// A stop-sign icon appears in the message box.
/// </summary>
IconError = IconStop,
/// <summary>
/// A stop-sign icon appears in the message box.
/// </summary>
IconHand = IconStop,
// To indicate the default button, specify one of the following values.
/// <summary>
/// The first button is the default button.
/// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
/// </summary>
DefButton1 = DefaultValue,
/// <summary>
/// The second button is the default button.
/// </summary>
DefButton2 = 0x100,
/// <summary>
/// The third button is the default button.
/// </summary>
DefButton3 = 0x200,
/// <summary>
/// The fourth button is the default button.
/// </summary>
DefButton4 = 0x300,
// To indicate the modality of the dialog box, specify one of the following values.
/// <summary>
/// The user must respond to the message box before continuing work in the window identified by the hWnd parameter.
/// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy
/// of windows in the application, the user may be able to move to other windows within the thread. All child windows
/// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
/// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
/// </summary>
ApplModal = DefaultValue,
/// <summary>
/// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
/// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate
/// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with
/// windows other than those associated with hWnd.
/// </summary>
SystemModal = 0x1000,
/// <summary>
/// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the
/// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle
/// available but still needs to prevent input to other windows in the calling thread without suspending other threads.
/// </summary>
TaskModal = 0x2000,
// To specify other options, use one or more of the following values.
/// <summary>
/// Same as desktop of the interactive window station. For more information, see Window Stations. If the current
/// input desktop is not the default desktop, MessageBox does not return until the user switches to the default
/// desktop.
/// </summary>
DefaultDesktopOnly = 0x20000,
/// <summary>
/// The text is right-justified.
/// </summary>
Right = 0x80000,
/// <summary>
/// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
/// </summary>
RtlReading = 0x100000,
/// <summary>
/// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function
/// for the message box.
/// </summary>
SetForeground = 0x10000,
/// <summary>
/// The message box is created with the WS_EX_TOPMOST window style.
/// </summary>
Topmost = 0x40000,
/// <summary>
/// The caller is a service notifying the user of an event. The function displays a message box on the current active
/// desktop, even if there is no user logged on to the computer.
/// </summary>
ServiceNotification = 0x200000,
}
/// <summary>
/// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
/// such as status or error information. The message box returns an integer value that indicates which button the user
/// clicked.
/// </summary>
/// <param name="hWnd">
/// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no
/// owner window.
/// </param>
/// <param name="text">
/// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage
/// return and/or linefeed character between each line.
/// </param>
/// <param name="caption">
/// The dialog box title. If this parameter is NULL, the default title is Error.</param>
/// <param name="type">
/// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups
/// of flags.
/// </param>
/// <returns>
/// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or
/// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an
/// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK.
/// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
/// succeeds, the return value is one of the ID* enum values.
/// </returns>
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
}
/// <summary>
/// Native kernel32 functions.
/// </summary>
internal static partial class NativeFunctions
{
/// <summary>
/// MEM_* from memoryapi.
/// </summary>
[Flags]
public enum AllocationType
{
/// <summary>
/// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
/// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
/// </summary>
CoalescePlaceholders = 0x1,
/// <summary>
/// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
/// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
/// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
/// </summary>
PreservePlaceholder = 0x2,
/// <summary>
/// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
/// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents
/// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed.
/// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit
/// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the
/// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit
/// a page that is already committed does not cause the function to fail. This means that you can commit pages without
/// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave,
/// flAllocationType must be MEM_COMMIT.
/// </summary>
Commit = 0x1000,
/// <summary>
/// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory
/// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To
/// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation
/// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released.
/// </summary>
Reserve = 0x2000,
/// <summary>
/// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state.
/// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit
/// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported
/// when the lpAddress parameter provides the base address for an enclave.
/// </summary>
Decommit = 0x4000,
/// <summary>
/// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and
/// available for other allocations). After this operation, the pages are in the free state. If you specify this
/// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function
/// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the
/// region are committed currently, the function first decommits, and then releases them. The function does not
/// fail if you attempt to release pages that are in different states, some reserved and some committed. This means
/// that you can release a range of pages without first determining the current commitment state.
/// </summary>
Release = 0x8000,
/// <summary>
/// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages
/// should not be read from or written to the paging file. However, the memory block will be used again later, so
/// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee
/// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit
/// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect.
/// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns
/// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable
/// if it is mapped to a paging file.
/// </summary>
Reset = 0x80000,
/// <summary>
/// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier.
/// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to
/// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in
/// the specified address range is intact. If the function fails, at least some of the data in the address range
/// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
/// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
/// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
/// protection value, such as PAGE_NOACCESS.
/// </summary>
ResetUndo = 0x1000000,
/// <summary>
/// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must
/// be used with MEM_RESERVE and no other values.
/// </summary>
Physical = 0x400000,
/// <summary>
/// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when
/// there are many allocations.
/// </summary>
TopDown = 0x100000,
/// <summary>
/// Causes the system to track pages that are written to in the allocated region. If you specify this value, you
/// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region
/// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking
/// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region
/// until the region is freed.
/// </summary>
WriteWatch = 0x200000,
/// <summary>
/// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum.
/// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify
/// MEM_RESERVE and MEM_COMMIT.
/// </summary>
LargePages = 0x20000000,
}
/// <summary>
/// Unprefixed flags from CreateRemoteThread.
/// </summary>
[Flags]
public enum CreateThreadFlags
{
/// <summary>
/// The thread runs immediately after creation.
/// </summary>
RunImmediately = 0x0,
/// <summary>
/// The thread is created in a suspended state, and does not run until the ResumeThread function is called.
/// </summary>
CreateSuspended = 0x4,
/// <summary>
/// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size.
/// </summary>
StackSizeParamIsReservation = 0x10000,
}
/// <summary>
/// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess.
/// </summary>
[Flags]
public enum DuplicateOptions : uint
{
/// <summary>
/// Closes the source handle. This occurs regardless of any error status returned.
/// </summary>
CloseSource = 0x00000001,
/// <summary>
/// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle.
/// </summary>
SameAccess = 0x00000002,
}
/// <summary>
/// PAGE_* from memoryapi.
/// </summary>
[Flags]
public enum MemoryProtection
{
/// <summary>
/// Enables execute access to the committed region of pages. An attempt to write to the committed region results
/// in an access violation. This flag is not supported by the CreateFileMapping function.
/// </summary>
Execute = 0x10,
/// <summary>
/// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
/// results in an access violation.
/// </summary>
ExecuteRead = 0x20,
/// <summary>
/// Enables execute, read-only, or read/write access to the committed region of pages.
/// </summary>
ExecuteReadWrite = 0x40,
/// <summary>
/// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
/// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
/// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
/// supported by the VirtualAlloc or VirtualAllocEx functions.
/// </summary>
ExecuteWriteCopy = 0x80,
/// <summary>
/// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
/// region results in an access violation. This flag is not supported by the CreateFileMapping function.
/// </summary>
NoAccess = 0x01,
/// <summary>
/// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
/// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
/// region results in an access violation.
/// </summary>
ReadOnly = 0x02,
/// <summary>
/// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
/// attempting to execute code in the committed region results in an access violation.
/// </summary>
ReadWrite = 0x04,
/// <summary>
/// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
/// a committed copy-on-write page results in a private copy of the page being made for the process. The private
/// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
/// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
/// supported by the VirtualAlloc or VirtualAllocEx functions.
/// </summary>
WriteCopy = 0x08,
/// <summary>
/// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
/// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
/// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
/// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
/// or CreateFileMapping functions.
/// </summary>
TargetsInvalid = 0x40000000,
/// <summary>
/// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
/// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
/// will be maintained while the page protection changes. This flag is only valid when the protection changes to
/// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
/// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
/// targets for CFG.
/// </summary>
TargetsNoUpdate = TargetsInvalid,
/// <summary>
/// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
/// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
/// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
/// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
/// system service, the service typically returns a failure status indicator. This value cannot be used with
/// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
/// </summary>
Guard = 0x100,
/// <summary>
/// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
/// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
/// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
/// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
/// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
/// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
/// </summary>
NoCache = 0x200,
/// <summary>
/// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
/// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
/// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
/// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
/// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
/// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
/// </summary>
WriteCombine = 0x400,
}
/// <summary>
/// PROCESS_* from processthreadsapi.
/// </summary>
[Flags]
public enum ProcessAccessFlags : uint
{
/// <summary>
/// All possible access rights for a process object.
/// </summary>
AllAccess = 0x001F0FFF,
/// <summary>
/// Required to create a process.
/// </summary>
CreateProcess = 0x0080,
/// <summary>
/// Required to create a thread.
/// </summary>
CreateThread = 0x0002,
/// <summary>
/// Required to duplicate a handle using DuplicateHandle.
/// </summary>
DupHandle = 0x0040,
/// <summary>
/// Required to retrieve certain information about a process, such as its token, exit code,
/// and priority class (see OpenProcessToken).
/// </summary>
QueryInformation = 0x0400,
/// <summary>
/// Required to retrieve certain information about a process(see GetExitCodeProcess, GetPriorityClass, IsProcessInJob,
/// QueryFullProcessImageName). A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted
/// PROCESS_QUERY_LIMITED_INFORMATION.
/// </summary>
QueryLimitedInformation = 0x1000,
/// <summary>
/// Required to set certain information about a process, such as its priority class (see SetPriorityClass).
/// </summary>
SetInformation = 0x0200,
/// <summary>
/// Required to set memory limits using SetProcessWorkingSetSize.
/// </summary>
SetQuote = 0x0100,
/// <summary>
/// Required to suspend or resume a process.
/// </summary>
SuspendResume = 0x0800,
/// <summary>
/// Required to terminate a process using TerminateProcess.
/// </summary>
Terminate = 0x0001,
/// <summary>
/// Required to perform an operation on the address space of a process(see VirtualProtectEx and WriteProcessMemory).
/// </summary>
VmOperation = 0x0008,
/// <summary>
/// Required to read memory in a process using ReadProcessMemory.
/// </summary>
VmRead = 0x0010,
/// <summary>
/// Required to write to memory in a process using WriteProcessMemory.
/// </summary>
VmWrite = 0x0020,
/// <summary>
/// Required to wait for the process to terminate using the wait functions.
/// </summary>
Synchronize = 0x00100000,
}
/// <summary>
/// WAIT_* from synchapi.
/// </summary>
public enum WaitResult
{
/// <summary>
/// The specified object is a mutex object that was not released by the thread that owned the mutex object
/// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and
/// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you
/// should check it for consistency.
/// </summary>
Abandoned = 0x80,
/// <summary>
/// The state of the specified object is signaled.
/// </summary>
Object0 = 0x0,
/// <summary>
/// The time-out interval elapsed, and the object's state is nonsignaled.
/// </summary>
Timeout = 0x102,
/// <summary>
/// The function has failed. To get extended error information, call GetLastError.
/// </summary>
WAIT_FAILED = 0xFFFFFFF,
}
/// <summary>
/// Closes an open object handle.
/// </summary>
/// <param name="hObject">
/// A valid handle to an open object.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error
/// information, call GetLastError. If the application is running under a debugger, the function will throw an exception if it receives
/// either a handle value that is not valid or a pseudo-handle value. This can happen if you close a handle twice, or if you call
/// CloseHandle on a handle returned by the FindFirstFile function instead of calling the FindClose function.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
/// <summary>
/// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function
/// to create a thread that runs in the virtual address space of another process and optionally specify extended attributes.
/// </summary>
/// <param name="hProcess">
/// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD,
/// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without
/// these rights on certain platforms. For more information, see Process Security and Access Rights.
/// </param>
/// <param name="lpThreadAttributes">
/// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether
/// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor
/// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from
/// the primary token of the creator.
/// </param>
/// <param name="dwStackSize">
/// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the
/// new thread uses the default size for the executable. For more information, see Thread Stack Size.
/// </param>
/// <param name="lpStartAddress">
/// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the
/// starting address of the thread in the remote process. The function must exist in the remote process. For more information,
/// see ThreadProc.
/// </param>
/// <param name="lpParameter">
/// A pointer to a variable to be passed to the thread function.
/// </param>
/// <param name="dwCreationFlags">
/// The flags that control the creation of the thread.
/// </param>
/// <param name="lpThreadId">
/// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is
/// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress
/// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and
/// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process.
/// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to
/// invalid or missing dynamic-link libraries (DLL).
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
UIntPtr dwStackSize,
nuint lpStartAddress,
nuint lpParameter,
CreateThreadFlags dwCreationFlags,
out uint lpThreadId);
/// <summary>
/// Retrieves the termination status of the specified thread.
/// </summary>
/// <param name="hThread">
/// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION
/// access right.For more information, see Thread Security and Access Rights.
/// </param>
/// <param name="lpExitCode">
/// A pointer to a variable to receive the thread termination status.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get
/// extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
/// <summary>
/// Opens an existing local process object.
/// </summary>
/// <param name="dwDesiredAccess">
/// The access to the process object. This access right is checked against the security descriptor for the process. This parameter can be one or
/// more of the process access rights. If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the
/// contents of the security descriptor.
/// </param>
/// <param name="bInheritHandle">
/// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.
/// </param>
/// <param name="dwProcessId">
/// The identifier of the local process to be opened. If the specified process is the System Idle Process(0x00000000), the function fails and the
/// last error code is ERROR_INVALID_PARAMETER.If the specified process is the System process or one of the Client Server Run-Time Subsystem(CSRSS)
/// processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from
/// opening them. If you are using GetCurrentProcessId as an argument to this function, consider using GetCurrentProcess instead of OpenProcess, for
/// improved performance.
/// </param>
/// <returns>
/// If the function succeeds, the return value is an open handle to the specified process.
/// If the function fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
ProcessAccessFlags dwDesiredAccess,
bool bInheritHandle,
int dwProcessId);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex.
/// Reserves, commits, or changes the state of a region of memory within the virtual address space of a specified process.
/// The function initializes the memory it allocates to zero. To specify the NUMA node for the physical memory, see
/// VirtualAllocExNuma.
/// </summary>
/// <param name="hProcess">
/// The handle to a process. The function allocates memory within the virtual address space of this process. The handle
/// must have the PROCESS_VM_OPERATION access right. For more information, see Process Security and Access Rights.
/// </param>
/// <param name="lpAddress">
/// The pointer that specifies a desired starting address for the region of pages that you want to allocate. If you
/// are reserving memory, the function rounds this address down to the nearest multiple of the allocation granularity.
/// If you are committing memory that is already reserved, the function rounds this address down to the nearest page
/// boundary. To determine the size of a page and the allocation granularity on the host computer, use the GetSystemInfo
/// function. If lpAddress is NULL, the function determines where to allocate the region. If this address is within
/// an enclave that you have not initialized by calling InitializeEnclave, VirtualAllocEx allocates a page of zeros
/// for the enclave at that address. The page must be previously uncommitted, and will not be measured with the EEXTEND
/// instruction of the Intel Software Guard Extensions programming model. If the address in within an enclave that you
/// initialized, then the allocation operation fails with the ERROR_INVALID_ADDRESS error.
/// </param>
/// <param name="dwSize">
/// The size of the region of memory to allocate, in bytes. If lpAddress is NULL, the function rounds dwSize up to the
/// next page boundary. If lpAddress is not NULL, the function allocates all pages that contain one or more bytes in
/// the range from lpAddress to lpAddress+dwSize. This means, for example, that a 2-byte range that straddles a page
/// boundary causes the function to allocate both pages.
/// </param>
/// <param name="flAllocationType">
/// The type of memory allocation. This parameter must contain one of the MEM_* enum values.
/// </param>
/// <param name="flProtect">
/// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify
/// any one of the memory protection constants.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the base address of the allocated region of pages. If the function
/// fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
int dwSize,
AllocationType flAllocationType,
MemoryProtection flProtect);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex.
/// Releases, decommits, or releases and decommits a region of memory within the virtual address space of a specified
/// process.
/// </summary>
/// <param name="hProcess">
/// A handle to a process. The function frees memory within the virtual address space of the process. The handle must
/// have the PROCESS_VM_OPERATION access right.For more information, see Process Security and Access Rights.
/// </param>
/// <param name="lpAddress">
/// A pointer to the starting address of the region of memory to be freed. If the dwFreeType parameter is MEM_RELEASE,
/// lpAddress must be the base address returned by the VirtualAllocEx function when the region is reserved.
/// </param>
/// <param name="dwSize">
/// The size of the region of memory to free, in bytes. If the dwFreeType parameter is MEM_RELEASE, dwSize must be 0
/// (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAllocEx.
/// If dwFreeType is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes in the range
/// from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of memory that
/// straddles a page boundary causes both pages to be decommitted. If lpAddress is the base address returned by
/// VirtualAllocEx and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAllocEx.
/// After that, the entire region is in the reserved state.
/// </param>
/// <param name="dwFreeType">
/// The type of free operation. This parameter must be one of the MEM_* enum values.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero).
/// To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern bool VirtualFreeEx(
IntPtr hProcess,
IntPtr lpAddress,
int dwSize,
AllocationType dwFreeType);
/// <summary>
/// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait
/// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects.
/// </summary>
/// <param name="hHandle">
/// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section.
/// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the
/// SYNCHRONIZE access right. For more information, see Standard Access Rights.
/// </param>
/// <param name="dwMilliseconds">
/// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled
/// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled;
/// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled.
/// </param>
/// <returns>
/// If the function succeeds, the return value indicates the event that caused the function to return.
/// It can be one of the WaitResult values.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
/// <summary>
/// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
/// the operation fails.
/// </summary>
/// <param name="hProcess">
/// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access
/// to the process.
/// </param>
/// <param name="lpBaseAddress">
/// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the
/// system verifies that all data in the base address and memory of the specified size is accessible for write access,
/// and if it is not accessible, the function fails.
/// </param>
/// <param name="lpBuffer">
/// A pointer to the buffer that contains data to be written in the address space of the specified process.
/// </param>
/// <param name="dwSize">
/// The number of bytes to be written to the specified process.
/// </param>
/// <param name="lpNumberOfBytesWritten">
/// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter
/// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
/// extended error information, call GetLastError.The function fails if the requested write operation crosses into an
/// area of the process that is inaccessible.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesWritten);
/// <summary>
/// Duplicates an object handle.
/// </summary>
/// <param name="hSourceProcessHandle">
/// A handle to the process with the handle to be duplicated.
///
/// The handle must have the PROCESS_DUP_HANDLE access right.
/// </param>
/// <param name="hSourceHandle">
/// The handle to be duplicated. This is an open object handle that is valid in the context of the source process.
/// For a list of objects whose handles can be duplicated, see the following Remarks section.
/// </param>
/// <param name="hTargetProcessHandle">
/// A handle to the process that is to receive the duplicated handle.
///
/// The handle must have the PROCESS_DUP_HANDLE access right.
/// </param>
/// <param name="lpTargetHandle">
/// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process.
///
/// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.
///
/// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates.
///
/// This parameter is ignored if hTargetProcessHandle is NULL.
/// </param>
/// <param name="dwDesiredAccess">
/// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section.
///
/// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated.
///
/// This parameter is ignored if hTargetProcessHandle is NULL.
/// </param>
/// <param name="bInheritHandle">
/// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited.
///
/// This parameter is ignored if hTargetProcessHandle is NULL.
/// </param>
/// <param name="dwOptions">
/// Optional actions.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero.
///
/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
/// </returns>
/// <remarks>
/// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle.
/// </remarks>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DuplicateHandle(
IntPtr hSourceProcessHandle,
IntPtr hSourceHandle,
IntPtr hTargetProcessHandle,
out IntPtr lpTargetHandle,
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
DuplicateOptions dwOptions);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
/// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
/// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
/// </summary>
/// <param name="lpModuleName">
/// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default
/// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate
/// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure
/// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules
/// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns
/// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve
/// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
/// value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetModuleHandleW(string lpModuleName);
/// <summary>
/// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
/// </summary>
/// <param name="hModule">
/// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
/// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
/// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <param name="procName">
/// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
/// in the low-order word; the high-order word must be zero.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the address of the exported function or variable. If the function
/// fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
[SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
}
}

View file

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

View file

@ -0,0 +1,8 @@
CreateRemoteThread
WaitForSingleObject
GetExitCodeThread
DuplicateHandle
MessageBox
GetModuleHandle
GetProcAddress

View file

@ -42,19 +42,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.analyzers" Version="0.10.0" />
<PackageReference Include="xunit.assert" Version="2.4.1" />
<PackageReference Include="xunit.core" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
<PackageReference Include="xunit.runner.console" Version="2.4.1">
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.abstractions" />
<PackageReference Include="xunit.analyzers" />
<PackageReference Include="xunit.assert" />
<PackageReference Include="xunit.core" />
<PackageReference Include="xunit.extensibility.core" />
<PackageReference Include="xunit.extensibility.execution" />
<PackageReference Include="xunit.runner.console">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -1,6 +1,10 @@
using System;
using System;
using System.IO;
using Dalamud.Configuration;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Xunit;
namespace Dalamud.Test.Game.Text.SeStringHandling
@ -50,19 +54,41 @@ namespace Dalamud.Test.Game.Text.SeStringHandling
var config = new MockConfig { Text = seString };
PluginConfigurations.SerializeConfig(config);
}
[Fact]
public void TestConfigDeserializable()
{
var builder = new SeStringBuilder();
var seString = builder.AddText("Some text").Build();
var config = new MockConfig { Text = seString };
// This relies on the type information being maintained, which is why we're using these
// static methods instead of default serialization/deserialization.
var configSerialized = PluginConfigurations.SerializeConfig(config);
var configDeserialized = (MockConfig)PluginConfigurations.DeserializeConfig(configSerialized);
Assert.Equal(config, configDeserialized);
}
[Theory]
[InlineData(49, 209)]
[InlineData(71, 7)]
[InlineData(62, 116)]
public void TestAutoTranslatePayloadReencode(uint group, uint key)
{
var payload = new AutoTranslatePayload(group, key);
Assert.Equal(group, payload.Group);
Assert.Equal(key, payload.Key);
var encoded = payload.Encode();
using var stream = new MemoryStream(encoded);
using var reader = new BinaryReader(stream);
var decodedPayload = Payload.Decode(reader) as AutoTranslatePayload;
Assert.Equal(group, decodedPayload.Group);
Assert.Equal(key, decodedPayload.Key);
Assert.Equal(encoded, decodedPayload.Encode());
}
}
}

View file

@ -31,19 +31,19 @@ public class ReliableFileStorageTests
.Select(
i => Parallel.ForEachAsync(
Enumerable.Range(1, 100),
(j, _) =>
async (j, _) =>
{
if (i % 2 == 0)
{
// ReSharper disable once AccessToDisposedClosure
rfs.Instance.WriteAllText(tempFile, j.ToString());
await rfs.Instance.WriteAllTextAsync(tempFile, j.ToString());
}
else if (i % 3 == 0)
{
try
{
// ReSharper disable once AccessToDisposedClosure
rfs.Instance.ReadAllText(tempFile);
await rfs.Instance.ReadAllTextAsync(tempFile);
}
catch (FileNotFoundException)
{
@ -54,8 +54,6 @@ public class ReliableFileStorageTests
{
File.Delete(tempFile);
}
return ValueTask.CompletedTask;
})));
}
@ -112,41 +110,41 @@ public class ReliableFileStorageTests
}
[Fact]
public void Exists_WhenFileInBackup_ReturnsTrue()
public async Task Exists_WhenFileInBackup_ReturnsTrue()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.True(rfs.Instance.Exists(tempFile));
}
[Fact]
public void Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
public async Task Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.False(rfs.Instance.Exists(tempFile, Guid.NewGuid()));
}
[Fact]
public void WriteAllText_ThrowsIfPathIsEmpty()
public async Task WriteAllText_ThrowsIfPathIsEmpty()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentException>(() => rfs.Instance.WriteAllText("", TestFileContent1));
await Assert.ThrowsAsync<ArgumentException>(async () => await rfs.Instance.WriteAllTextAsync("", TestFileContent1));
}
[Fact]
public void WriteAllText_ThrowsIfPathIsNull()
public async Task WriteAllText_ThrowsIfPathIsNull()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentNullException>(() => rfs.Instance.WriteAllText(null!, TestFileContent1));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await rfs.Instance.WriteAllTextAsync(null!, TestFileContent1));
}
[Fact]
@ -155,26 +153,26 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
Assert.True(File.Exists(tempFile));
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
}
[Fact]
public void WriteAllText_SeparatesContainers()
public async Task WriteAllText_SeparatesContainers()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
var containerId = Guid.NewGuid();
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
rfs.Instance.WriteAllText(tempFile, TestFileContent2, containerId);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2, containerId);
File.Delete(tempFile);
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true, containerId));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true, containerId));
}
[Fact]
@ -183,7 +181,7 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateFailedRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
Assert.True(File.Exists(tempFile));
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
@ -195,38 +193,38 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
rfs.Instance.WriteAllText(tempFile, TestFileContent2);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2);
Assert.True(File.Exists(tempFile));
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, await File.ReadAllTextAsync(tempFile));
}
[Fact]
public void WriteAllText_SupportsNullContent()
public async Task WriteAllText_SupportsNullContent()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, null);
await rfs.Instance.WriteAllTextAsync(tempFile, null);
Assert.True(File.Exists(tempFile));
Assert.Equal("", rfs.Instance.ReadAllText(tempFile));
Assert.Equal("", await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
public void ReadAllText_ThrowsIfPathIsEmpty()
public async Task ReadAllText_ThrowsIfPathIsEmpty()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentException>(() => rfs.Instance.ReadAllText(""));
await Assert.ThrowsAsync<ArgumentException>(async () => await rfs.Instance.ReadAllTextAsync(""));
}
[Fact]
public void ReadAllText_ThrowsIfPathIsNull()
public async Task ReadAllText_ThrowsIfPathIsNull()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentNullException>(() => rfs.Instance.ReadAllText(null!));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await rfs.Instance.ReadAllTextAsync(null!));
}
[Fact]
@ -236,40 +234,40 @@ public class ReliableFileStorageTests
await File.WriteAllTextAsync(tempFile, TestFileContent1);
using var rfs = CreateRfs();
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
public void ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
public async Task ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
public void ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
public async Task ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
var containerId = Guid.NewGuid();
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, containerId: containerId));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, containerId: containerId));
}
[Fact]
public void ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
public async Task ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateFailedRfs();
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
@ -278,7 +276,7 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
await File.WriteAllTextAsync(tempFile, TestFileContent1);
using var rfs = CreateRfs();
rfs.Instance.ReadAllText(tempFile, text => Assert.Equal(TestFileContent1, text));
await rfs.Instance.ReadAllTextAsync(tempFile, text => Assert.Equal(TestFileContent1, text));
}
[Fact]
@ -290,7 +288,7 @@ public class ReliableFileStorageTests
var readerCalledOnce = false;
using var rfs = CreateRfs();
Assert.Throws<FileReadException>(() => rfs.Instance.ReadAllText(tempFile, Reader));
await Assert.ThrowsAsync<FileReadException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, Reader));
return;
@ -303,7 +301,7 @@ public class ReliableFileStorageTests
}
[Fact]
public void ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
public async Task ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
@ -311,10 +309,10 @@ public class ReliableFileStorageTests
var assertionCalled = false;
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
rfs.Instance.ReadAllText(tempFile, Reader);
await rfs.Instance.ReadAllTextAsync(tempFile, Reader);
Assert.True(assertionCalled);
return;
@ -335,17 +333,17 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
await File.WriteAllTextAsync(tempFile, TestFileContent1);
using var rfs = CreateRfs();
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, _ => throw new FileNotFoundException()));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, _ => throw new FileNotFoundException()));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
public async Task ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, forceBackup));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup));
}
private static DisposableReliableFileStorage CreateRfs()

View file

@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
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
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
@ -74,6 +75,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandaloneImGuiTestbed", "i
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiScene", "imgui\ImGuiScene\ImGuiScene.csproj", "{66753AC7-0029-4373-9CC4-7760B1F46141}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lumina", "Lumina", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "lib\Lumina.Excel\src\Lumina.Excel.Generator\Lumina.Excel.Generator.csproj", "{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -168,6 +175,14 @@ Global
{66753AC7-0029-4373-9CC4-7760B1F46141}.Debug|Any CPU.Build.0 = Debug|x64
{66753AC7-0029-4373-9CC4-7760B1F46141}.Release|Any CPU.ActiveCfg = Release|x64
{66753AC7-0029-4373-9CC4-7760B1F46141}.Release|Any CPU.Build.0 = Release|x64
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Release|Any CPU.Build.0 = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -191,6 +206,9 @@ Global
{9C70BD06-D52C-425E-9C14-5D66BC6046EF} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{4702A911-2513-478C-A434-2776393FDE77} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{66753AC7-0029-4373-9CC4-7760B1F46141} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -108,11 +108,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool DoPluginTest { get; set; } = false;
/// <summary>
/// Gets or sets a key to opt into Dalamud staging builds.
/// </summary>
public string? DalamudBetaKey { get; set; } = null;
/// <summary>
/// Gets or sets a list of custom repos.
/// </summary>
@ -250,7 +245,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// This setting is effected by the in-game "System Sounds" option and volume.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; }
public bool EnablePluginUISoundEffects { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether an additional button allowing pinning and clickthrough options should be shown
@ -278,11 +273,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>
public string? DalamudBetaKind { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
/// It is reset immediately when read.
@ -497,19 +487,27 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
#pragma warning disable SA1600
#pragma warning disable SA1516
// XLCore/XoM compatibility until they move it out
public string? DalamudBetaKey { get; set; } = null;
public string? DalamudBetaKind { get; set; }
#pragma warning restore SA1516
#pragma warning restore SA1600
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
/// <param name="path">Path to read from.</param>
/// <param name="fs">File storage.</param>
/// <returns>The deserialized configuration file.</returns>
public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
public static async Task<DalamudConfiguration> Load(string path, ReliableFileStorage fs)
{
DalamudConfiguration deserialized = null;
try
{
fs.ReadAllText(path, text =>
await fs.ReadAllTextAsync(path, text =>
{
deserialized =
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
@ -580,8 +578,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
this.Save();
this.isSaveQueued = false;
Log.Verbose("Config saved");
}
}
@ -630,16 +626,20 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
// Wait for previous write to finish
this.writeTask?.Wait();
this.writeTask = Task.Run(() =>
this.writeTask = Task.Run(async () =>
{
Service<ReliableFileStorage>.Get().WriteAllText(
this.configPath,
JsonConvert.SerializeObject(this, SerializerSettings));
await Service<ReliableFileStorage>.Get().WriteAllTextAsync(
this.configPath,
JsonConvert.SerializeObject(this, SerializerSettings));
Log.Verbose("DalamudConfiguration saved");
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath);
Log.Error(
t.Exception,
"Failed to save DalamudConfiguration to {Path}",
this.configPath);
}
});

View file

@ -2,6 +2,8 @@ using System.IO;
using System.Reflection;
using Dalamud.Storage;
using Dalamud.Utility;
using Newtonsoft.Json;
namespace Dalamud.Configuration;
@ -9,6 +11,7 @@ namespace Dalamud.Configuration;
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </summary>
[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
@ -36,7 +39,7 @@ public sealed class PluginConfigurations
public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId)
{
Service<ReliableFileStorage>.Get()
.WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
.WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId).GetAwaiter().GetResult();
}
/// <summary>
@ -52,12 +55,12 @@ public sealed class PluginConfigurations
IPluginConfiguration? config = null;
try
{
Service<ReliableFileStorage>.Get().ReadAllText(path.FullName, text =>
Service<ReliableFileStorage>.Get().ReadAllTextAsync(path.FullName, text =>
{
config = DeserializeConfig(text);
if (config == null)
throw new Exception("Read config was null.");
}, workingPluginId);
}, workingPluginId).GetAwaiter().GetResult();
}
catch (FileNotFoundException)
{

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>13.0.0.0</DalamudVersion>
<DalamudVersion>13.0.0.12</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -61,38 +61,38 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BitFaster.Caching" Version="2.4.1" />
<PackageReference Include="CheapLoc" Version="1.1.8" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp5" />
<PackageReference Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp3" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PackageReference Include="BitFaster.Caching" />
<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" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PackageReference Include="MinSharp" />
<PackageReference Include="SharpDX.Direct3D11" />
<PackageReference Include="SharpDX.Mathematics" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
<PackageReference Include="System.Resources.Extensions" Version="8.0.0" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
<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>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>
@ -101,6 +101,7 @@
<LogicalName>imgui-vertex.hlsl.bytes</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\imgui\Dalamud.Bindings.ImGuizmo\Dalamud.Bindings.ImGuizmo.csproj" />
@ -109,6 +110,7 @@
<ProjectReference Include="..\imgui\ImGuiScene\ImGuiScene.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj" />
<ProjectReference Include="..\lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj" />
</ItemGroup>
<ItemGroup>
@ -160,6 +162,9 @@
<Exec Command="git -C &quot;$(ProjectDir.Replace('\','\\'))&quot; describe --tags --always --dirty" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitDescribeOutput" />
</Exec>
<Exec Command="git -C &quot;$(ProjectDir.Replace('\','\\'))&quot; rev-parse --abbrev-ref HEAD" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitBranch" />
</Exec>
<Exec Command="git -C &quot;$(ProjectDir.Replace('\','\\'))\..\lib\FFXIVClientStructs&quot; describe --long --always --dirty" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="ClientStructsGitDescribeOutput" />
</Exec>
@ -167,6 +172,7 @@
<PropertyGroup>
<CommitCount>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))</CommitCount>
<CommitHash>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", ""))</CommitHash>
<Branch>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitBranch), @"\t|\n|\r", ""))</Branch>
<SCMVersion>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))</SCMVersion>
<CommitHashClientStructs>$([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))</CommitHashClientStructs>
</PropertyGroup>
@ -180,6 +186,7 @@
<!-- stub out version since it takes a while. -->
<PropertyGroup>
<SCMVersion>Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))</SCMVersion>
<Branch>???</Branch>
<CommitHashClientStructs>???</CommitHashClientStructs>
</PropertyGroup>
</Target>
@ -203,6 +210,10 @@
<_Parameter1>GitCommitCount</_Parameter1>
<_Parameter2>$(CommitCount)</_Parameter2>
</AssemblyAttributes>
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(Branch)' != ''">
<_Parameter1>GitBranch</_Parameter1>
<_Parameter2>$(Branch)</_Parameter2>
</AssemblyAttributes>
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(CommitHashClientStructs)' != ''">
<_Parameter1>GitHashClientStructs</_Parameter1>
<_Parameter2>$(CommitHashClientStructs)</_Parameter2>

View file

@ -41,7 +41,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
try
{
Log.Verbose("Starting data load...");
using (Timings.Start("Lumina Init"))
{
var luminaOptions = new LuminaOptions
@ -53,12 +53,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
DefaultExcelLanguage = this.Language.ToLumina(),
};
this.GameData = new(
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
luminaOptions)
try
{
StreamPool = new(),
};
this.GameData = new(
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
luminaOptions)
{
StreamPool = new(),
};
}
catch (Exception ex)
{
Log.Error(ex, "Lumina GameData init failed");
Util.Fatal(
"Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" +
"Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".",
"Dalamud");
return;
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
@ -71,7 +84,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
dalamud.StartInfo.TroubleshootingPackData);
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);
}
@ -130,7 +143,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
#region Lumina Wrappers
/// <inheritdoc/>
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
=> this.Excel.GetSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
@ -138,7 +151,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
=> this.Excel.GetSubrowSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
public FileResource? GetFile(string path)
public FileResource? GetFile(string path)
=> this.GetFile<FileResource>(path);
/// <inheritdoc/>
@ -161,7 +174,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
: Task.FromException<T>(new FileNotFoundException("The file could not be found."));
/// <inheritdoc/>
public bool FileExists(string path)
public bool FileExists(string path)
=> this.GameData.FileExists(path);
#endregion

View file

@ -144,7 +144,8 @@ public sealed class EntryPoint
// Load configuration first to get some early persistent state, like log level
var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!);
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs);
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs)
.GetAwaiter().GetResult();
// Set the appropriate logging level from the configuration
if (!configuration.LogSynchronously)

View file

@ -74,7 +74,7 @@ public enum AddonEventType : byte
/// <summary>
/// Resize (ChatLogPanel).
/// </summary>
Resize = 19,
Resize = 21,
/// <summary>
/// AtkComponentButton Press, sent on MouseDown on Button.

View file

@ -38,7 +38,7 @@ public abstract unsafe class AddonArgs
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
internal bool IsAddon(ReadOnlySpan<char> name)
internal bool IsAddon(string name)
{
if (this.Addon.IsNull)
return false;
@ -46,12 +46,10 @@ public abstract unsafe class AddonArgs
if (name.Length is 0 or > 32)
return false;
var addonName = this.Addon.Name;
if (string.IsNullOrEmpty(addonName))
if (string.IsNullOrEmpty(this.Addon.Name))
return false;
return name == addonName;
return name == this.Addon.Name;
}
/// <summary>

View file

@ -77,7 +77,7 @@ internal partial class ChatHandlers : IServiceType
}
// For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
if (clientState.IsLoggedIn && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
#if !DEBUG && false
@ -124,11 +124,11 @@ internal partial class ChatHandlers : IServiceType
var updateMessage = new SeStringBuilder()
.AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!"))
.AddUiForeground(500)
.AddText(" [")
.AddText(" [ ")
.Add(linkPayload)
.AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog."))
.AddText(Loc.Localize("DalamudClickToViewChangelogs", "Click here to view the changelog."))
.Add(RawPayload.LinkTerminator)
.AddText("]")
.AddText(" ]")
.AddUiForegroundOff();
chatGui.Print(new XivChatEntry

View file

@ -97,7 +97,7 @@ internal sealed class AetheryteEntry : IAetheryteEntry
public uint GilCost => this.data.GilCost;
/// <inheritdoc />
public bool IsFavourite => this.data.IsFavourite != 0;
public bool IsFavourite => this.data.IsFavourite;
/// <inheritdoc />
public bool IsSharedHouse => this.data.IsSharedHouse;

View file

@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -22,7 +23,7 @@ namespace Dalamud.Game.ClientState.Aetherytes;
internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private readonly Telepo* telepoInstance = Telepo.Instance();
@ -37,7 +38,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
{
get
{
if (this.clientState.LocalPlayer == null)
if (this.objectTable.LocalPlayer == null)
return 0;
this.Update();
@ -59,7 +60,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
return null;
}
if (this.clientState.LocalPlayer == null)
if (this.objectTable.LocalPlayer == null)
return null;
return new AetheryteEntry(this.telepoInstance->TeleportList[index]);
@ -69,7 +70,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
private void Update()
{
// this is very very important as otherwise it crashes
if (this.clientState.LocalPlayer == null)
if (this.objectTable.LocalPlayer == null)
return;
this.telepoInstance->UpdateAetheryteList();

View file

@ -2,11 +2,13 @@ using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Game.Player;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy;
@ -24,7 +26,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
private const uint InvalidObjectID = 0xE0000000;
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly PlayerState playerState = Service<PlayerState>.Get();
[ServiceManager.ServiceConstructor]
private BuddyList()
@ -69,7 +71,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy;
/// <inheritdoc/>
public IBuddyMember? this[int index]
@ -105,10 +107,10 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
/// <inheritdoc/>
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
if (address == IntPtr.Zero)
return null;
if (address == IntPtr.Zero)
if (!this.playerState.IsLoaded)
return null;
var buddy = new BuddyMember(address);

View file

@ -19,8 +19,14 @@ public interface IBuddyMember
/// <summary>
/// Gets the object ID of this buddy.
/// </summary>
[Obsolete("Renamed to EntityId")]
uint ObjectId { get; }
/// <summary>
/// Gets the entity ID of this buddy.
/// </summary>
uint EntityId { get; }
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
@ -83,6 +89,9 @@ internal unsafe class BuddyMember : IBuddyMember
/// <inheritdoc />
public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc />
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc />
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);

View file

@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui;
using Dalamud.Game.Network.Internal;
using Dalamud.Game.Player;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
@ -15,14 +16,14 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Network;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.Sheets;
using Action = System.Action;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState;
@ -36,9 +37,9 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;
private readonly Hook<EventFramework.Delegates.SetTerritoryTypeId> setupTerritoryTypeHook;
private readonly Hook<HandleZoneInitPacketDelegate> handleZoneInitPacketHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<LogoutCallbackInterface.Delegates.OnLogout> onLogoutHook;
private readonly Hook<SetCurrentInstanceDelegate> setCurrentInstanceHook;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
@ -46,6 +47,18 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
[ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
[ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get();
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private Hook<LogoutCallbackInterface.Delegates.OnLogout> onLogoutHook;
private bool initialized;
private ushort territoryTypeId;
private bool isPvP;
private uint mapId;
private uint instance;
private bool lastConditionNone = true;
[ServiceManager.ServiceConstructor]
@ -59,26 +72,37 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
var setTerritoryTypeAddr = EventFramework.Addresses.SetTerritoryTypeId.Value;
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}");
this.setupTerritoryTypeHook = Hook<EventFramework.Delegates.SetTerritoryTypeId>.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour);
this.handleZoneInitPacketHook = Hook<HandleZoneInitPacketDelegate>.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.onLogoutHook = Hook<LogoutCallbackInterface.Delegates.OnLogout>.FromAddress((nint)LogoutCallbackInterface.StaticVirtualTablePointer->OnLogout, this.OnLogoutDetour);
this.setCurrentInstanceHook = Hook<SetCurrentInstanceDelegate>.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour);
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable();
this.handleZoneInitPacketHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.onLogoutHook.Enable();
this.setCurrentInstanceHook.Enable();
this.framework.RunOnTick(this.Setup);
}
private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet);
private unsafe delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type);
private unsafe delegate void SetCurrentInstanceDelegate(NetworkModuleProxy* thisPtr, short instanceId);
/// <inheritdoc/>
public event Action<ZoneInitEventArgs> ZoneInit;
/// <inheritdoc/>
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event Action<uint>? MapIdChanged;
/// <inheritdoc/>
public event Action<uint>? InstanceChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
@ -104,23 +128,73 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public ClientLanguage ClientLanguage { get; }
/// <inheritdoc/>
public ushort TerritoryType { get; private set; }
/// <inheritdoc/>
public unsafe uint MapId
public ushort TerritoryType
{
get
get => this.territoryTypeId;
private set
{
var agentMap = AgentMap.Instance();
return agentMap != null ? agentMap->CurrentMapId : 0;
if (this.territoryTypeId != value)
{
this.territoryTypeId = value;
if (this.initialized)
{
Log.Debug("TerritoryType changed: {0}", value);
this.TerritoryChanged?.InvokeSafely(value);
}
var rowRef = LuminaUtils.CreateRef<TerritoryType>(value);
if (rowRef.IsValid)
{
this.IsPvP = rowRef.Value.IsPvpZone;
}
}
}
}
/// <inheritdoc/>
public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter;
public uint MapId
{
get => this.mapId;
private set
{
if (this.mapId != value)
{
this.mapId = value;
if (this.initialized)
{
Log.Debug("MapId changed: {0}", value);
this.MapIdChanged?.InvokeSafely(value);
}
}
}
}
/// <inheritdoc/>
public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId;
public uint Instance
{
get => this.instance;
private set
{
if (this.instance != value)
{
this.instance = value;
if (this.initialized)
{
Log.Debug("Instance changed: {0}", value);
this.InstanceChanged?.InvokeSafely(value);
}
}
}
}
/// <inheritdoc/>
public IPlayerCharacter? LocalPlayer => this.objectTable.LocalPlayer;
/// <inheritdoc/>
public unsafe ulong LocalContentId => this.playerState.ContentId;
/// <inheritdoc/>
public unsafe bool IsLoggedIn
@ -133,7 +207,31 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
}
/// <inheritdoc/>
public bool IsPvP { get; private set; }
public bool IsPvP
{
get => this.isPvP;
private set
{
if (this.isPvP != value)
{
this.isPvP = value;
if (this.initialized)
{
if (value)
{
Log.Debug("EnterPvP");
this.EnterPvP?.InvokeSafely();
}
else
{
Log.Debug("LeavePvP");
this.LeavePvP?.InvokeSafely();
}
}
}
}
}
/// <inheritdoc/>
public bool IsPvPExcludingDen => this.IsPvP && this.TerritoryType != 250;
@ -150,7 +248,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public bool IsClientIdle(out ConditionFlag blockingFlag)
{
blockingFlag = 0;
if (this.LocalPlayer is null) return true;
if (this.objectTable.LocalPlayer is null) return true;
var condition = Service<Conditions.Condition>.GetNullable();
@ -158,7 +256,8 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.UsingFashionAccessory]);
ConditionFlag.UsingFashionAccessory,
ConditionFlag.OnFreeTrial]);
blockingFlag = blockingConditions.FirstOrDefault();
return blockingFlag == 0;
@ -172,43 +271,44 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// </summary>
void IInternalDisposableService.DisposeService()
{
this.setupTerritoryTypeHook.Dispose();
this.handleZoneInitPacketHook.Dispose();
this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose();
this.setCurrentInstanceHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.framework.Update -= this.OnFrameworkUpdate;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType)
private unsafe void Setup()
{
Log.Debug("TerritoryType changed: {0}", territoryType);
this.onLogoutHook = Hook<LogoutCallbackInterface.Delegates.OnLogout>.FromAddress((nint)AgentLobby.Instance()->LogoutCallbackInterface.VirtualTable->OnLogout, this.OnLogoutDetour);
this.onLogoutHook.Enable();
this.TerritoryType = territoryType;
this.TerritoryChanged?.InvokeSafely(territoryType);
this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId;
this.MapId = AgentMap.Instance()->CurrentMapId;
this.Instance = CSUIState.Instance()->PublicInstance.InstanceId;
var rowRef = LuminaUtils.CreateRef<TerritoryType>(territoryType);
if (rowRef.IsValid)
this.initialized = true;
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 isPvP = rowRef.Value.IsPvpZone;
if (isPvP != this.IsPvP)
{
this.IsPvP = isPvP;
if (this.IsPvP)
{
Log.Debug("EnterPvP");
this.EnterPvP?.InvokeSafely();
}
else
{
Log.Debug("LeavePvP");
this.LeavePvP?.InvokeSafely();
}
}
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");
}
this.setupTerritoryTypeHook.Original(eventFramework, territoryType);
}
private unsafe void UIModuleHandlePacketDetour(
@ -259,8 +359,16 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
}
}
private void FrameworkOnOnUpdateEvent(IFramework framework1)
private unsafe void SetCurrentInstanceDetour(NetworkModuleProxy* thisPtr, short instanceId)
{
this.setCurrentInstanceHook.Original(thisPtr, instanceId);
this.Instance = (uint)instanceId;
}
private unsafe void OnFrameworkUpdate(IFramework framework)
{
this.MapId = AgentMap.Instance()->CurrentMapId;
var condition = Service<Conditions.Condition>.GetNullable();
var gameGui = Service<GameGui>.GetNullable();
var data = Service<DataManager>.GetNullable();
@ -268,7 +376,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
if (condition == null || gameGui == null || data == null)
return;
if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null)
if (condition.Any() && this.lastConditionNone && this.objectTable.LocalPlayer != null)
{
Log.Debug("Is login");
this.lastConditionNone = false;
@ -342,7 +450,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// </summary>
internal ClientStatePluginScoped()
{
this.clientStateService.ZoneInit += this.ZoneInitForward;
this.clientStateService.TerritoryChanged += this.TerritoryChangedForward;
this.clientStateService.MapIdChanged += this.MapIdChangedForward;
this.clientStateService.InstanceChanged += this.InstanceChangedForward;
this.clientStateService.ClassJobChanged += this.ClassJobChangedForward;
this.clientStateService.LevelChanged += this.LevelChangedForward;
this.clientStateService.Login += this.LoginForward;
@ -352,9 +463,18 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
this.clientStateService.CfPop += this.ContentFinderPopForward;
}
/// <inheritdoc/>
public event Action<ZoneInitEventArgs> ZoneInit;
/// <inheritdoc/>
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event Action<uint>? MapIdChanged;
/// <inheritdoc/>
public event Action<uint>? InstanceChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
@ -385,6 +505,9 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// <inheritdoc/>
public uint MapId => this.clientStateService.MapId;
/// <inheritdoc/>
public uint Instance => this.clientStateService.Instance;
/// <inheritdoc/>
public IPlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer;
@ -412,7 +535,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.clientStateService.ZoneInit -= this.ZoneInitForward;
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
this.clientStateService.MapIdChanged -= this.MapIdChangedForward;
this.clientStateService.InstanceChanged -= this.InstanceChangedForward;
this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward;
this.clientStateService.LevelChanged -= this.LevelChangedForward;
this.clientStateService.Login -= this.LoginForward;
@ -421,7 +547,12 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
this.clientStateService.LeavePvP -= this.ExitPvPForward;
this.clientStateService.CfPop -= this.ContentFinderPopForward;
this.ZoneInit = null;
this.TerritoryChanged = null;
this.MapIdChanged = null;
this.InstanceChanged = null;
this.ClassJobChanged = null;
this.LevelChanged = null;
this.Login = null;
this.Logout = null;
this.EnterPvP = null;
@ -429,8 +560,14 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
this.CfPop = null;
}
private void ZoneInitForward(ZoneInitEventArgs eventArgs) => this.ZoneInit?.Invoke(eventArgs);
private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId);
private void MapIdChangedForward(uint mapId) => this.MapIdChanged?.Invoke(mapId);
private void InstanceChangedForward(uint instanceId) => this.InstanceChanged?.Invoke(instanceId);
private void ClassJobChangedForward(uint classJobId) => this.ClassJobChanged?.Invoke(classJobId);
private void LevelChangedForward(uint classJobId, uint level) => this.LevelChanged?.Invoke(classJobId, level);

View file

@ -10,19 +10,24 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <summary>
/// Gets the address of the keyboard state.
/// </summary>
public IntPtr KeyboardState { get; private set; }
public nint KeyboardState { get; private set; }
/// <summary>
/// Gets the address of the keyboard state index array which translates the VK enumeration to the key state.
/// </summary>
public IntPtr KeyboardStateIndexArray { get; private set; }
public nint KeyboardStateIndexArray { get; private set; }
// Functions
/// <summary>
/// Gets the address of the method which sets up the player.
/// Gets the address of the method that handles the ZoneInit packet.
/// </summary>
public IntPtr ProcessPacketPlayerSetup { get; private set; }
public nint HandleZoneInitPacket { get; private set; }
/// <summary>
/// Gets the address of the method that sets the current public instance.
/// </summary>
public nint SetCurrentInstance { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
@ -30,7 +35,8 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); // not in cs struct
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.
// lea rcx, ds:1DB9F74h[rax*4] KeyboardState

View file

@ -65,7 +65,11 @@ public enum ConditionFlag
/// </summary>
RidingPillion = 10,
[Obsolete("Renamed to RidingPillion", true)] Mounted2 = 10,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
[Obsolete("Renamed to RidingPillion", true)]
Mounted2 = 10,
/// <summary>
/// Unable to execute command while in that position.
@ -429,7 +433,11 @@ public enum ConditionFlag
/// </summary>
MountImmobile = 88,
[Obsolete("Renamed to MountImmobile", true)] InThisState88 = 88,
/// <summary>
/// Unable to execute command in this state.
/// </summary>
[Obsolete("Renamed to MountImmobile", true)]
InThisState88 = 88,
/// <summary>
/// Unable to execute command in this state.

View file

@ -1,6 +1,7 @@
using System.Numerics;
using Dalamud.Data;
using Dalamud.Game.Player;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
@ -150,15 +151,11 @@ internal unsafe partial class Fate
/// <returns>True or false.</returns>
public static bool IsValid(Fate fate)
{
var clientState = Service<ClientState>.GetNullable();
if (fate == null || clientState == null)
if (fate == null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
var playerState = Service<PlayerState>.Get();
return playerState.IsLoaded == true;
}
/// <summary>

View file

@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using Dalamud.Game.Player;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -60,15 +61,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable
/// <inheritdoc/>
public bool IsValid(IFate fate)
{
var clientState = Service<ClientState>.GetNullable();
if (fate == null || clientState == null)
if (fate == null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
var playerState = Service<PlayerState>.Get();
return playerState.IsLoaded == true;
}
/// <inheritdoc/>
@ -87,12 +84,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable
/// <inheritdoc/>
public IFate? CreateFateReference(IntPtr offset)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
if (offset == IntPtr.Zero)
return null;
if (offset == IntPtr.Zero)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new Fate(offset);

View file

@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Player;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -31,16 +32,16 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
private static int objectTableLength;
private readonly ClientState clientState;
[ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get();
private readonly CachedEntry[] cachedObjectTable;
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable(ClientState clientState)
private unsafe ObjectTable()
{
this.clientState = clientState;
var nativeObjectTable = CSGameObjectManager.Instance()->Objects.IndexSorted;
objectTableLength = nativeObjectTable.Length;
@ -66,6 +67,9 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
public int Length => objectTableLength;
/// <inheritdoc/>
public IPlayerCharacter? LocalPlayer => this[0] as IPlayerCharacter;
/// <inheritdoc/>
public IEnumerable<IBattleChara> PlayerObjects => this.GetPlayerObjects();
@ -142,10 +146,10 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
ThreadSafety.AssertMainThread();
if (this.clientState.LocalContentId == 0)
if (address == nint.Zero)
return null;
if (address == nint.Zero)
if (!this.playerState.IsLoaded)
return null;
var obj = (CSGameObject*)address;

View file

@ -77,10 +77,10 @@ internal unsafe class BattleChara : Character, IBattleChara
public StatusList StatusList => new(this.Struct->GetStatusManager());
/// <inheritdoc/>
public bool IsCasting => this.Struct->GetCastInfo()->IsCasting > 0;
public bool IsCasting => this.Struct->GetCastInfo()->IsCasting;
/// <inheritdoc/>
public bool IsCastInterruptible => this.Struct->GetCastInfo()->Interruptible > 0;
public bool IsCastInterruptible => this.Struct->GetCastInfo()->Interruptible;
/// <inheritdoc/>
public byte CastActionType => (byte)this.Struct->GetCastInfo()->ActionType;

View file

@ -1,9 +1,8 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Player;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Objects.Types;
@ -35,8 +34,14 @@ public interface IGameObject : IEquatable<IGameObject>
/// <summary>
/// Gets the data ID for linking to other respective game data.
/// </summary>
[Obsolete("Renamed to BaseId")]
public uint DataId { get; }
/// <summary>
/// Gets the base ID for linking to other respective game data.
/// </summary>
public uint BaseId { get; }
/// <summary>
/// Gets the ID of this GameObject's owner.
/// </summary>
@ -164,15 +169,11 @@ internal partial class GameObject
/// <returns>True or false.</returns>
public static bool IsValid(IGameObject? actor)
{
var clientState = Service<ClientState>.GetNullable();
if (actor is null || clientState == null)
if (actor == null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
var playerState = Service<PlayerState>.Get();
return playerState.IsLoaded == true;
}
/// <summary>
@ -208,6 +209,9 @@ internal unsafe partial class GameObject : IGameObject
/// <inheritdoc/>
public uint DataId => this.Struct->BaseId;
/// <inheritdoc/>
public uint BaseId => this.Struct->BaseId;
/// <inheritdoc/>
public uint OwnerId => this.Struct->OwnerId;

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game.Player;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -25,7 +26,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
private const int AllianceLength = 20;
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly PlayerState playerState = Service<PlayerState>.Get();
[ServiceManager.ServiceConstructor]
private PartyList()
@ -91,10 +92,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
/// <inheritdoc/>
public IPartyMember? CreatePartyMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
return null;
return new PartyMember(address);
@ -112,10 +110,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
/// <inheritdoc/>
public IPartyMember? CreateAllianceMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
return null;
return new PartyMember(address);

View file

@ -40,8 +40,14 @@ public interface IPartyMember
/// <summary>
/// Gets the actor ID of this party member.
/// </summary>
[Obsolete("Renamed to EntityId")]
uint ObjectId { get; }
/// <summary>
/// Gets the entity ID of this party member.
/// </summary>
uint EntityId { get; }
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
@ -115,87 +121,55 @@ internal unsafe class PartyMember : IPartyMember
this.Address = address;
}
/// <summary>
/// Gets the address of this party member in memory.
/// </summary>
/// <inheritdoc/>
public IntPtr Address { get; }
/// <summary>
/// Gets a list of buffs or debuffs applied to this party member.
/// </summary>
/// <inheritdoc/>
public StatusList Statuses => new(&this.Struct->StatusManager);
/// <summary>
/// Gets the position of the party member.
/// </summary>
public Vector3 Position => new(this.Struct->X, this.Struct->Y, this.Struct->Z);
/// <inheritdoc/>
public Vector3 Position => this.Struct->Position;
/// <summary>
/// Gets the content ID of the party member.
/// </summary>
/// <inheritdoc/>
public long ContentId => (long)this.Struct->ContentId;
/// <summary>
/// Gets the actor ID of this party member.
/// </summary>
/// <inheritdoc/>
public uint ObjectId => this.Struct->EntityId;
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.ObjectId);
/// <inheritdoc/>
public uint EntityId => this.Struct->EntityId;
/// <summary>
/// Gets the current HP of this party member.
/// </summary>
/// <inheritdoc/>
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
/// <inheritdoc/>
public uint CurrentHP => this.Struct->CurrentHP;
/// <summary>
/// Gets the maximum HP of this party member.
/// </summary>
/// <inheritdoc/>
public uint MaxHP => this.Struct->MaxHP;
/// <summary>
/// Gets the current MP of this party member.
/// </summary>
/// <inheritdoc/>
public ushort CurrentMP => this.Struct->CurrentMP;
/// <summary>
/// Gets the maximum MP of this party member.
/// </summary>
/// <inheritdoc/>
public ushort MaxMP => this.Struct->MaxMP;
/// <summary>
/// Gets the territory this party member is located in.
/// </summary>
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
/// <summary>
/// Gets the World this party member resides in.
/// </summary>
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
/// <summary>
/// Gets the displayname of this party member.
/// </summary>
/// <inheritdoc/>
public SeString Name => SeString.Parse(this.Struct->Name);
/// <summary>
/// Gets the sex of this party member.
/// </summary>
/// <inheritdoc/>
public byte Sex => this.Struct->Sex;
/// <summary>
/// Gets the classjob of this party member.
/// </summary>
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
/// <summary>
/// Gets the level of this party member.
/// </summary>
/// <inheritdoc/>
public byte Level => this.Struct->Level;
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;

View file

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game.Player;
namespace Dalamud.Game.ClientState.Statuses;
/// <summary>
@ -66,15 +68,14 @@ public sealed unsafe partial class StatusList
/// <returns>The status object containing the requested data.</returns>
public static StatusList? CreateStatusListReference(IntPtr address)
{
if (address == IntPtr.Zero)
return null;
// 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 == IntPtr.Zero)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new StatusList(address);
@ -87,12 +88,11 @@ public sealed unsafe partial class StatusList
/// <returns>The status object containing the requested data.</returns>
public static Status? CreateStatusReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
if (address == IntPtr.Zero)
return null;
if (address == IntPtr.Zero)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new Status(address);

View file

@ -0,0 +1,90 @@
using System.Linq;
using System.Text;
using Dalamud.Data;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.ClientState;
/// <summary>
/// Provides event data for when the game should initialize a zone.
/// </summary>
public class ZoneInitEventArgs : EventArgs
{
/// <summary>
/// Gets the territory type of the zone being entered.
/// </summary>
public TerritoryType TerritoryType { get; private set; }
/// <summary>
/// Gets the instance number of the zone, used when multiple copies of an area are active.
/// </summary>
public ushort Instance { get; private set; }
/// <summary>
/// Gets the associated content finder condition for the zone, if any.
/// </summary>
public ContentFinderCondition ContentFinderCondition { get; private set; }
/// <summary>
/// Gets the current weather in the zone upon entry.
/// </summary>
public Weather Weather { get; private set; }
/// <summary>
/// Gets the set of active festivals in the zone.
/// </summary>
public Festival[] ActiveFestivals { get; private set; } = [];
/// <summary>
/// Gets the phases corresponding to the active festivals.
/// </summary>
public ushort[] ActiveFestivalPhases { get; private set; } = [];
/// <summary>
/// Reads raw zone initialization data from a network packet and constructs the event arguments.
/// </summary>
/// <param name="packet">A pointer to the raw packet data.</param>
/// <returns>A <see cref="ZoneInitEventArgs"/> populated from the packet.</returns>
public static unsafe ZoneInitEventArgs Read(nint packet)
{
var dataManager = Service<DataManager>.Get();
var eventArgs = new ZoneInitEventArgs();
var flags = *(byte*)(packet + 0x12);
eventArgs.TerritoryType = dataManager.GetExcelSheet<TerritoryType>().GetRow(*(ushort*)(packet + 0x02));
eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04);
eventArgs.ContentFinderCondition = dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(*(ushort*)(packet + 0x06));
eventArgs.Weather = dataManager.GetExcelSheet<Weather>().GetRow(*(byte*)(packet + 0x10));
const int NumFestivals = 4;
eventArgs.ActiveFestivals = new Festival[NumFestivals];
eventArgs.ActiveFestivalPhases = new ushort[NumFestivals];
// There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively,
// but it's unclear why they exist as separate entries and why they would be different.
for (var i = 0; i < NumFestivals; i++)
{
eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet<Festival>().GetRow(*(ushort*)(packet + 0x2E + (i * 2)));
eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2));
}
return eventArgs;
}
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder("ZoneInitEventArgs { ");
sb.Append($"TerritoryTypeId = {this.TerritoryType.RowId}, ");
sb.Append($"Instance = {this.Instance}, ");
sb.Append($"ContentFinderCondition = {this.ContentFinderCondition.RowId}, ");
sb.Append($"Weather = {this.Weather.RowId}, ");
sb.Append($"ActiveFestivals = [{string.Join(", ", this.ActiveFestivals.Select(f => f.RowId))}], ");
sb.Append($"ActiveFestivalPhases = [{string.Join(", ", this.ActiveFestivalPhases)}]");
sb.Append(" }");
return sb.ToString();
}
}

View file

@ -1031,6 +1031,13 @@ public enum SystemConfigOption
[GameConfigOption("TitleScreenType", ConfigType.UInt)]
TitleScreenType,
/// <summary>
/// System option with the internal name DisplayObjectLimitType2.
/// This option is a UInt.
/// </summary>
[GameConfigOption("DisplayObjectLimitType2", ConfigType.UInt)]
DisplayObjectLimitType2,
/// <summary>
/// System option with the internal name AccessibilitySoundVisualEnable.
/// This option is a UInt.
@ -1115,6 +1122,13 @@ public enum SystemConfigOption
[GameConfigOption("CameraZoom", ConfigType.UInt)]
CameraZoom,
/// <summary>
/// System option with the internal name DynamicAroundRangeMode.
/// This option is a UInt.
/// </summary>
[GameConfigOption("DynamicAroundRangeMode", ConfigType.UInt)]
DynamicAroundRangeMode,
/// <summary>
/// System option with the internal name DynamicRezoType.
/// This option is a UInt.

View file

@ -2032,6 +2032,13 @@ public enum UiConfigOption
[GameConfigOption("NamePlateDispJobIconInInstanceOther", ConfigType.UInt)]
NamePlateDispJobIconInInstanceOther,
/// <summary>
/// UiConfig option with the internal name LogNamePlateDispEnemyCast.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogNamePlateDispEnemyCast", ConfigType.UInt)]
LogNamePlateDispEnemyCast,
/// <summary>
/// UiConfig option with the internal name ActiveInfo.
/// This option is a UInt.
@ -2690,6 +2697,594 @@ public enum UiConfigOption
[GameConfigOption("LogColorOtherClass", ConfigType.UInt)]
LogColorOtherClass,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleEnableChatBubble.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleEnableChatBubble", ConfigType.UInt)]
LogChatBubbleEnableChatBubble,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowMax.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowMax", ConfigType.UInt)]
LogChatBubbleShowMax,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowOwnMessage.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowOwnMessage", ConfigType.UInt)]
LogChatBubbleShowOwnMessage,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowDuringBattle.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowDuringBattle", ConfigType.UInt)]
LogChatBubbleShowDuringBattle,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSizeType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSizeType", ConfigType.UInt)]
LogChatBubbleSizeType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowLargePvP.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowLargePvP", ConfigType.UInt)]
LogChatBubbleShowLargePvP,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowQuickChat.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowQuickChat", ConfigType.UInt)]
LogChatBubbleShowQuickChat,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleDispRowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleDispRowType", ConfigType.UInt)]
LogChatBubbleDispRowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSayShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSayShowType", ConfigType.UInt)]
LogChatBubbleSayShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSayFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSayFontColor", ConfigType.UInt)]
LogChatBubbleSayFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSayWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSayWindowColor", ConfigType.UInt)]
LogChatBubbleSayWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleYellShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleYellShowType", ConfigType.UInt)]
LogChatBubbleYellShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleYellFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleYellFontColor", ConfigType.UInt)]
LogChatBubbleYellFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleYellWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleYellWindowColor", ConfigType.UInt)]
LogChatBubbleYellWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShoutShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShoutShowType", ConfigType.UInt)]
LogChatBubbleShoutShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShoutFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShoutFontColor", ConfigType.UInt)]
LogChatBubbleShoutFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShoutWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShoutWindowColor", ConfigType.UInt)]
LogChatBubbleShoutWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleTellShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleTellShowType", ConfigType.UInt)]
LogChatBubbleTellShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleTellFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleTellFontColor", ConfigType.UInt)]
LogChatBubbleTellFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleTellWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleTellWindowColor", ConfigType.UInt)]
LogChatBubbleTellWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePartyShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePartyShowType", ConfigType.UInt)]
LogChatBubblePartyShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePartyFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePartyFontColor", ConfigType.UInt)]
LogChatBubblePartyFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePartyWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePartyWindowColor", ConfigType.UInt)]
LogChatBubblePartyWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleAllianceShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleAllianceShowType", ConfigType.UInt)]
LogChatBubbleAllianceShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleAllianceFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleAllianceFontColor", ConfigType.UInt)]
LogChatBubbleAllianceFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleAllianceWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleAllianceWindowColor", ConfigType.UInt)]
LogChatBubbleAllianceWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleFcShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleFcShowType", ConfigType.UInt)]
LogChatBubbleFcShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleFcFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleFcFontColor", ConfigType.UInt)]
LogChatBubbleFcFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleFcWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleFcWindowColor", ConfigType.UInt)]
LogChatBubbleFcWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleBeginnerShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleBeginnerShowType", ConfigType.UInt)]
LogChatBubbleBeginnerShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleBeginnerFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleBeginnerFontColor", ConfigType.UInt)]
LogChatBubbleBeginnerFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleBeginnerWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleBeginnerWindowColor", ConfigType.UInt)]
LogChatBubbleBeginnerWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePvpteamShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePvpteamShowType", ConfigType.UInt)]
LogChatBubblePvpteamShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePvpteamFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePvpteamFontColor", ConfigType.UInt)]
LogChatBubblePvpteamFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePvpteamWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePvpteamWindowColor", ConfigType.UInt)]
LogChatBubblePvpteamWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs1ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs1ShowType", ConfigType.UInt)]
LogChatBubbleLs1ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs1FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs1FontColor", ConfigType.UInt)]
LogChatBubbleLs1FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs1WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs1WindowColor", ConfigType.UInt)]
LogChatBubbleLs1WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs2ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs2ShowType", ConfigType.UInt)]
LogChatBubbleLs2ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs2FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs2FontColor", ConfigType.UInt)]
LogChatBubbleLs2FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs2WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs2WindowColor", ConfigType.UInt)]
LogChatBubbleLs2WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs3ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs3ShowType", ConfigType.UInt)]
LogChatBubbleLs3ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs3FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs3FontColor", ConfigType.UInt)]
LogChatBubbleLs3FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs3WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs3WindowColor", ConfigType.UInt)]
LogChatBubbleLs3WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs4ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs4ShowType", ConfigType.UInt)]
LogChatBubbleLs4ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs4FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs4FontColor", ConfigType.UInt)]
LogChatBubbleLs4FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs4WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs4WindowColor", ConfigType.UInt)]
LogChatBubbleLs4WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs5ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs5ShowType", ConfigType.UInt)]
LogChatBubbleLs5ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs5FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs5FontColor", ConfigType.UInt)]
LogChatBubbleLs5FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs5WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs5WindowColor", ConfigType.UInt)]
LogChatBubbleLs5WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs6ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs6ShowType", ConfigType.UInt)]
LogChatBubbleLs6ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs6FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs6FontColor", ConfigType.UInt)]
LogChatBubbleLs6FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs6WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs6WindowColor", ConfigType.UInt)]
LogChatBubbleLs6WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs7ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs7ShowType", ConfigType.UInt)]
LogChatBubbleLs7ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs7FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs7FontColor", ConfigType.UInt)]
LogChatBubbleLs7FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs7WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs7WindowColor", ConfigType.UInt)]
LogChatBubbleLs7WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs8ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs8ShowType", ConfigType.UInt)]
LogChatBubbleLs8ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs8FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs8FontColor", ConfigType.UInt)]
LogChatBubbleLs8FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs8WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs8WindowColor", ConfigType.UInt)]
LogChatBubbleLs8WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls1ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls1ShowType", ConfigType.UInt)]
LogChatBubbleCwls1ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls1FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls1FontColor", ConfigType.UInt)]
LogChatBubbleCwls1FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls1WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls1WindowColor", ConfigType.UInt)]
LogChatBubbleCwls1WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls2ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls2ShowType", ConfigType.UInt)]
LogChatBubbleCwls2ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls2FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls2FontColor", ConfigType.UInt)]
LogChatBubbleCwls2FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls2WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls2WindowColor", ConfigType.UInt)]
LogChatBubbleCwls2WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls3ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls3ShowType", ConfigType.UInt)]
LogChatBubbleCwls3ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls3FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls3FontColor", ConfigType.UInt)]
LogChatBubbleCwls3FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls3WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls3WindowColor", ConfigType.UInt)]
LogChatBubbleCwls3WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls4ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls4ShowType", ConfigType.UInt)]
LogChatBubbleCwls4ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls4FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls4FontColor", ConfigType.UInt)]
LogChatBubbleCwls4FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls4WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls4WindowColor", ConfigType.UInt)]
LogChatBubbleCwls4WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls5ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls5ShowType", ConfigType.UInt)]
LogChatBubbleCwls5ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls5FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls5FontColor", ConfigType.UInt)]
LogChatBubbleCwls5FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls5WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls5WindowColor", ConfigType.UInt)]
LogChatBubbleCwls5WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls6ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls6ShowType", ConfigType.UInt)]
LogChatBubbleCwls6ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls6FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls6FontColor", ConfigType.UInt)]
LogChatBubbleCwls6FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls6WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls6WindowColor", ConfigType.UInt)]
LogChatBubbleCwls6WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls7ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls7ShowType", ConfigType.UInt)]
LogChatBubbleCwls7ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls7FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls7FontColor", ConfigType.UInt)]
LogChatBubbleCwls7FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls7WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls7WindowColor", ConfigType.UInt)]
LogChatBubbleCwls7WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls8ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls8ShowType", ConfigType.UInt)]
LogChatBubbleCwls8ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls8FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls8FontColor", ConfigType.UInt)]
LogChatBubbleCwls8FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls8WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls8WindowColor", ConfigType.UInt)]
LogChatBubbleCwls8WindowColor,
/// <summary>
/// UiConfig option with the internal name LogPermeationRateInput.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogPermeationRateInput", ConfigType.UInt)]
LogPermeationRateInput,
/// <summary>
/// UiConfig option with the internal name ChatType.
/// This option is a UInt.
@ -3453,6 +4048,27 @@ public enum UiConfigOption
[GameConfigOption("GPoseMotionFilterAction", ConfigType.UInt)]
GPoseMotionFilterAction,
/// <summary>
/// UiConfig option with the internal name GPoseRollRotationCameraCorrection.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GPoseRollRotationCameraCorrection", ConfigType.UInt)]
GPoseRollRotationCameraCorrection,
/// <summary>
/// UiConfig option with the internal name GPoseInitCameraFocus.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GPoseInitCameraFocus", ConfigType.UInt)]
GPoseInitCameraFocus,
/// <summary>
/// UiConfig option with the internal name GposePortraitRotateType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GposePortraitRotateType", ConfigType.UInt)]
GposePortraitRotateType,
/// <summary>
/// UiConfig option with the internal name LsListSortPriority.
/// This option is a UInt.

View file

@ -0,0 +1,34 @@
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace Dalamud.Game.Gui;
/// <summary>
/// Represents a flag set by the game used by agents to conditionally update their addons.
/// </summary>
[Flags]
public enum AgentUpdateFlag : byte
{
/// <summary> Set when an inventory has been updated. </summary>
InventoryUpdate = 1 << 0,
/// <summary> Set when a hotbar slot has been executed, or a Gearset or Macro has been changed. </summary>
ActionBarUpdate = 1 << 1,
/// <summary> Set when the RetainerMarket inventory has been updated. </summary>
RetainerMarketInventoryUpdate = 1 << 2,
// /// <summary> Unknown use case. </summary>
// NameplateUpdate = 1 << 3,
/// <summary> Set when the player unlocked collectibles, contents or systems. </summary>
UnlocksUpdate = 1 << 4,
/// <summary> Set when <see cref="AgentHUD.SetMainCommandEnabledState"/> was called. </summary>
MainCommandEnabledStateUpdate = 1 << 5,
/// <summary> Set when any housing inventory has been updated. </summary>
HousingInventoryUpdate = 1 << 6,
/// <summary> Set when any content inventory has been updated. </summary>
ContentInventoryUpdate = 1 << 7,
}

View file

@ -41,7 +41,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private static readonly ModuleLog Log = new("ChatGui");
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, Guid CommandId), Action<Guid, SeString>> dalamudLinkHandlers = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
@ -50,7 +50,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private ImmutableDictionary<(string PluginName, Guid CommandId), Action<Guid, SeString>>? dalamudLinkHandlersCopy;
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
private uint dalamudChatHandlerId = 1000;
[ServiceManager.ServiceConstructor]
private ChatGui()
@ -86,7 +87,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
public byte LastLinkedItemFlags { get; private set; }
/// <inheritdoc/>
public IReadOnlyDictionary<(string PluginName, Guid CommandId), Action<Guid, SeString>> RegisteredLinkHandlers
public IReadOnlyDictionary<(string PluginName, uint CommandId), Action<uint, SeString>> RegisteredLinkHandlers
{
get
{
@ -164,19 +165,33 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
#region Chat Links
/// <inheritdoc/>
public DalamudLinkPayload AddChatLinkHandler(Action<Guid, SeString> commandAction)
/// <summary>
/// Register a chat link handler.
/// </summary>
/// <remarks>Internal use only.</remarks>
/// <param name="commandAction">The action to be executed.</param>
/// <returns>Returns an SeString payload for the link.</returns>
public DalamudLinkPayload AddChatLinkHandler(Action<uint, SeString> commandAction)
{
return this.AddChatLinkHandler("Dalamud", commandAction);
return this.AddChatLinkHandler("Dalamud", this.dalamudChatHandlerId++, commandAction);
}
/// <inheritdoc/>
public void RemoveChatLinkHandler(Guid commandId)
/// <remarks>Internal use only.</remarks>
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction)
{
return this.AddChatLinkHandler("Dalamud", commandId, commandAction);
}
/// <inheritdoc/>
/// <remarks>Internal use only.</remarks>
public void RemoveChatLinkHandler(uint commandId)
{
this.RemoveChatLinkHandler("Dalamud", commandId);
}
/// <inheritdoc/>
/// <remarks>Internal use only.</remarks>
public void RemoveChatLinkHandler()
{
this.RemoveChatLinkHandler("Dalamud");
@ -240,11 +255,11 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
/// Create a link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns>
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, Action<Guid, SeString> commandAction)
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
var commandId = Guid.CreateVersion7();
var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId };
lock (this.dalamudLinkHandlers)
{
@ -277,7 +292,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to be removed.</param>
internal void RemoveChatLinkHandler(string pluginName, Guid commandId)
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
lock (this.dalamudLinkHandlers)
{
@ -400,13 +415,13 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
if (!terminatedSender.SequenceEqual(possiblyModifiedSenderData))
{
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(terminatedSender)} -> {parsedSender}");
Log.Verbose($"HandlePrintMessageDetour Sender modified: {new ReadOnlySeStringSpan(terminatedSender).ToMacroString()} -> {new ReadOnlySeStringSpan(possiblyModifiedSenderData).ToMacroString()}");
sender->SetString(possiblyModifiedSenderData);
}
if (!terminatedMessage.SequenceEqual(possiblyModifiedMessageData))
{
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(terminatedMessage)} -> {parsedMessage}");
Log.Verbose($"HandlePrintMessageDetour Message modified: {new ReadOnlySeStringSpan(terminatedMessage).ToMacroString()} -> {new ReadOnlySeStringSpan(possiblyModifiedMessageData).ToMacroString()}");
message->SetString(possiblyModifiedMessageData);
}
@ -534,7 +549,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags;
/// <inheritdoc/>
public IReadOnlyDictionary<(string PluginName, Guid CommandId), Action<Guid, SeString>> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers;
public IReadOnlyDictionary<(string PluginName, uint CommandId), Action<uint, SeString>> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -551,11 +566,11 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
}
/// <inheritdoc/>
public DalamudLinkPayload AddChatLinkHandler(Action<Guid, SeString> commandAction)
=> this.chatGuiService.AddChatLinkHandler(this.plugin.InternalName, commandAction);
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction)
=> this.chatGuiService.AddChatLinkHandler(this.plugin.InternalName, commandId, commandAction);
/// <inheritdoc/>
public void RemoveChatLinkHandler(Guid commandId)
public void RemoveChatLinkHandler(uint commandId)
=> this.chatGuiService.RemoveChatLinkHandler(this.plugin.InternalName, commandId);
/// <inheritdoc/>

View file

@ -451,14 +451,14 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
case ContextMenuType.Default:
{
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, &this.SelectedAgent->AtkEventInterface, 71, checked((ushort)ownerAddonId), 4);
break;
}
case ContextMenuType.Inventory:
{
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, &this.SelectedAgent->AtkEventInterface, 0, checked((ushort)ownerAddonId), 4);
break;
}

View file

@ -257,7 +257,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// <param name="toRemove">The resources to remove.</param>
internal void RemoveEntry(DtrBarEntry toRemove)
{
this.RemoveNode(toRemove.TextNode);
this.RemoveNode(toRemove);
if (toRemove.Storage != null)
{
@ -378,12 +378,12 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var isHide = !data.Shown || data.UserHidden;
var node = data.TextNode;
var nodeHidden = !node->AtkResNode.IsVisible();
var nodeHidden = !node->IsVisible();
if (!isHide)
{
if (nodeHidden)
node->AtkResNode.ToggleVisibility(true);
node->ToggleVisibility(true);
if (data is { Added: true, Text: not null, TextNode: not null } && (data.Dirty || nodeHidden))
{
@ -397,27 +397,27 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
ushort w = 0, h = 0;
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->AtkResNode.SetWidth(w);
node->SetWidth(w);
}
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing;
if (this.configuration.DtrSwapDirection)
{
data.TextNode->AtkResNode.SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2);
data.TextNode->SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2);
runningXPos += elementWidth;
}
else
{
runningXPos -= elementWidth;
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
data.TextNode->SetPositionFloat(runningXPos, 2);
}
}
else if (!nodeHidden)
{
// If we want the node hidden, shift it up, to prevent collision conflicts
node->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX);
node->AtkResNode.ToggleVisibility(false);
node->SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX);
node->ToggleVisibility(false);
}
data.Dirty = false;
@ -516,8 +516,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var node = data.TextNode = this.MakeNode(++this.runningNodeIds);
this.eventHandles.TryAdd(node->AtkResNode.NodeId, new List<IAddonEventHandle>());
this.eventHandles[node->AtkResNode.NodeId].AddRange(new List<IAddonEventHandle>
this.eventHandles.TryAdd(node->NodeId, new List<IAddonEventHandle>());
this.eventHandles[node->NodeId].AddRange(new List<IAddonEventHandle>
{
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler),
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler),
@ -528,8 +528,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
lastChild->PrevSiblingNode = (AtkResNode*)node;
node->AtkResNode.ParentNode = lastChild->ParentNode;
node->AtkResNode.NextSiblingNode = lastChild;
node->ParentNode = lastChild->ParentNode;
node->NextSiblingNode = lastChild;
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1);
Log.Debug("Set last sibling of DTR and updated child count");
@ -542,22 +542,31 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
return true;
}
private void RemoveNode(AtkTextNode* node)
private void RemoveNode(DtrBarEntry data)
{
var dtr = this.GetDtr();
var node = data.TextNode;
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return;
this.eventHandles[node->AtkResNode.NodeId].ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle));
this.eventHandles[node->AtkResNode.NodeId].Clear();
if (this.eventHandles.TryGetValue(node->NodeId, out var eventHandles))
{
eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle));
eventHandles.Clear();
}
else
{
Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->NodeId);
}
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
var tmpPrevNode = node->PrevSiblingNode;
var tmpNextNode = node->NextSiblingNode;
// if (tmpNextNode != null)
tmpNextNode->PrevSiblingNode = tmpPrevNode;
if (tmpPrevNode != null)
tmpPrevNode->NextSiblingNode = tmpNextNode;
node->AtkResNode.Destroy(true);
node->Destroy(true);
data.TextNode = null;
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1);
Log.Debug("Set last sibling of DTR and updated child count");
@ -575,19 +584,18 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
return null;
}
newTextNode->AtkResNode.NodeId = nodeId;
newTextNode->AtkResNode.Type = NodeType.Text;
newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents;
newTextNode->AtkResNode.DrawFlags = 12;
newTextNode->AtkResNode.SetWidth(22);
newTextNode->AtkResNode.SetHeight(22);
newTextNode->AtkResNode.SetPositionFloat(-200, 2);
newTextNode->NodeId = nodeId;
newTextNode->Type = NodeType.Text;
newTextNode->NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents;
newTextNode->DrawFlags = 12;
newTextNode->SetWidth(22);
newTextNode->SetHeight(22);
newTextNode->SetPositionFloat(-200, 2);
newTextNode->LineSpacing = 12;
newTextNode->AlignmentFontType = 5;
newTextNode->FontSize = 14;
newTextNode->TextFlags = (byte)TextFlags.Edge;
newTextNode->TextFlags2 = 0;
newTextNode->TextFlags = TextFlags.Edge;
if (this.emptyString == null)
this.emptyString = Utf8String.FromString(" ");
@ -642,7 +650,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
break;
case AddonEventType.MouseClick:
dtrBarEntry.OnClick?.Invoke(new AddonMouseEventData(eventData));
dtrBarEntry.OnClick?.Invoke(DtrInteractionEvent.FromMouseEvent(new AddonMouseEventData(eventData)));
break;
}
}

View file

@ -1,4 +1,6 @@
using Dalamud.Configuration.Internal;
using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Internal.Types;
@ -43,6 +45,16 @@ public interface IReadOnlyDtrBarEntry
/// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings.
/// </summary>
public bool UserHidden { get; }
/// <summary>
/// Gets an action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action<DtrInteractionEvent>? OnClick { get; }
/// <summary>
/// Gets the axis-aligned bounding box of this entry, in screen coordinates.
/// </summary>
public (Vector2 Min, Vector2 Max) ScreenBounds { get; }
}
/// <summary>
@ -68,7 +80,7 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// <summary>
/// Gets or sets an action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action<AddonMouseEventData>? OnClick { get; set; }
public new Action<DtrInteractionEvent>? OnClick { get; set; }
/// <summary>
/// Remove this entry from the bar.
@ -118,7 +130,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
public SeString? Tooltip { get; set; }
/// <inheritdoc/>
public Action<AddonMouseEventData>? OnClick { get; set; }
public Action<DtrInteractionEvent>? OnClick { get; set; }
/// <inheritdoc/>
public bool HasClickAction => this.OnClick != null;
@ -141,6 +153,17 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
[Api13ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <inheritdoc/>
public (Vector2 Min, Vector2 Max) ScreenBounds
=> this.TextNode switch
{
null => default,
var node => node->IsVisible()
? (new(node->ScreenX, node->ScreenY),
new(node->ScreenX + node->GetWidth(), node->ScreenY + node->GetHeight()))
: default,
};
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>

View file

@ -0,0 +1,59 @@
using System.Numerics;
using Dalamud.Game.Addon.Events.EventDataTypes;
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// Represents an interaction event from the DTR system.
/// </summary>
public class DtrInteractionEvent
{
/// <summary>
/// Gets the type of mouse click (left or right).
/// </summary>
public MouseClickType ClickType { get; init; }
/// <summary>
/// Gets the modifier keys that were held during the click.
/// </summary>
public ClickModifierKeys ModifierKeys { get; init; }
/// <summary>
/// Gets the scroll direction of the mouse wheel, if applicable.
/// </summary>
public MouseScrollDirection ScrollDirection { get; init; }
/// <summary>
/// Gets lower-level mouse data, if this event came from native UI.
///
/// Can only be set by Dalamud. If null, this event was manually created.
/// </summary>
public AddonMouseEventData? AtkEventSource { get; private init; }
/// <summary>
/// Gets the position of the mouse cursor when the event occurred.
/// </summary>
public Vector2 Position { get; init; }
/// <summary>
/// Helper to create a <see cref="DtrInteractionEvent"/> from an <see cref="AddonMouseEventData"/>.
/// </summary>
/// <param name="ev">The event.</param>
/// <returns>A better event.</returns>
public static DtrInteractionEvent FromMouseEvent(AddonMouseEventData ev)
{
return new DtrInteractionEvent
{
AtkEventSource = ev,
ClickType = ev.IsLeftClick ? MouseClickType.Left : MouseClickType.Right,
ModifierKeys = (ev.IsAltHeld ? ClickModifierKeys.Alt : 0) |
(ev.IsControlHeld ? ClickModifierKeys.Ctrl : 0) |
(ev.IsShiftHeld ? ClickModifierKeys.Shift : 0),
ScrollDirection = ev.IsScrollUp ? MouseScrollDirection.Up :
ev.IsScrollDown ? MouseScrollDirection.Down :
MouseScrollDirection.None,
Position = ev.Position,
};
}
}

View file

@ -0,0 +1,65 @@
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// An enum representing the mouse click types.
/// </summary>
public enum MouseClickType
{
/// <summary>
/// A left click.
/// </summary>
Left,
/// <summary>
/// A right click.
/// </summary>
Right,
}
/// <summary>
/// Modifier keys that can be held during a mouse click event.
/// </summary>
[Flags]
public enum ClickModifierKeys
{
/// <summary>
/// No modifiers were present.
/// </summary>
None = 0,
/// <summary>
/// The CTRL key was held.
/// </summary>
Ctrl = 1 << 0,
/// <summary>
/// The ALT key was held.
/// </summary>
Alt = 1 << 1,
/// <summary>
/// The SHIFT key was held.
/// </summary>
Shift = 1 << 2,
}
/// <summary>
/// Possible directions for scroll wheel events.
/// </summary>
public enum MouseScrollDirection
{
/// <summary>
/// No scrolling.
/// </summary>
None = 0,
/// <summary>
/// A scroll up event.
/// </summary>
Up = 1,
/// <summary>
/// A scroll down event.
/// </summary>
Down = -1,
}

View file

@ -43,6 +43,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
private readonly Hook<HandleImmDelegate> handleImmHook;
private readonly Hook<RaptureAtkModule.Delegates.SetUiVisibility> setUiVisibilityHook;
private readonly Hook<Utf8String.Delegates.Ctor_FromSequence> utf8StringFromSequenceHook;
private readonly Hook<RaptureAtkModule.Delegates.Update> raptureAtkModuleUpdateHook;
[ServiceManager.ServiceConstructor]
private GameGui(TargetSigScanner sigScanner)
@ -65,6 +66,10 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
this.utf8StringFromSequenceHook = Hook<Utf8String.Delegates.Ctor_FromSequence>.FromAddress(Utf8String.Addresses.Ctor_FromSequence.Value, this.Utf8StringFromSequenceDetour);
this.raptureAtkModuleUpdateHook = Hook<RaptureAtkModule.Delegates.Update>.FromFunctionPointerVariable(
new(&RaptureAtkModule.StaticVirtualTablePointer->Update),
this.RaptureAtkModuleUpdateDetour);
this.handleItemHoverHook.Enable();
this.handleItemOutHook.Enable();
this.handleImmHook.Enable();
@ -72,6 +77,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
this.handleActionHoverHook.Enable();
this.handleActionOutHook.Enable();
this.utf8StringFromSequenceHook.Enable();
this.raptureAtkModuleUpdateHook.Enable();
}
// Hooked delegates
@ -88,6 +94,9 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public event EventHandler<HoveredAction>? HoveredActionChanged;
/// <inheritdoc/>
public event Action<AgentUpdateFlag> AgentUpdate;
/// <inheritdoc/>
public bool GameUiHidden { get; private set; }
@ -182,6 +191,10 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return (nint)unitManager->GetAddonByName(name, index);
}
/// <inheritdoc/>
public T* GetAddonByName<T>(string name, int index = 1) where T : unmanaged
=> (T*)this.GetAddonByName(name, index).Address;
/// <inheritdoc/>
public AgentInterfacePtr GetAgentById(int id)
{
@ -234,6 +247,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose();
this.utf8StringFromSequenceHook.Dispose();
this.raptureAtkModuleUpdateHook.Dispose();
}
/// <summary>
@ -276,8 +290,6 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
this.HoveredItem = itemId;
this.HoveredItemChanged?.InvokeSafely(this, itemId);
Log.Verbose($"HoveredItem changed: {itemId}");
}
private AtkValue* HandleItemOutDetour(AgentItemDetail* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
@ -288,22 +300,18 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
{
this.HoveredItem = 0;
this.HoveredItemChanged?.InvokeSafely(this, 0ul);
Log.Verbose("HoveredItem changed: 0");
}
return ret;
}
private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, byte a5)
private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, bool a5, int a6, int a7)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5, a6, a7);
this.HoveredAction.ActionKind = (HoverActionKind)actionKind;
this.HoveredAction.BaseActionID = actionId;
this.HoveredAction.ActionID = hoverState->ActionId;
this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction);
Log.Verbose($"HoverActionId: {actionKind}/{actionId} this:{(nint)hoverState:X}");
}
private AtkValue* HandleActionOutDetour(AgentActionDetail* agentActionDetail, AtkValue* a2, AtkValue* a3, uint a4, ulong a5)
@ -320,15 +328,13 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
this.HoveredAction.BaseActionID = 0;
this.HoveredAction.ActionID = 0;
this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction);
Log.Verbose("HoverActionId: 0");
}
}
return retVal;
}
private unsafe void SetUiVisibilityDetour(RaptureAtkModule* thisPtr, bool uiVisible)
private void SetUiVisibilityDetour(RaptureAtkModule* thisPtr, bool uiVisible)
{
this.setUiVisibilityHook.Original(thisPtr, uiVisible);
@ -358,6 +364,21 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe?
}
private void RaptureAtkModuleUpdateDetour(RaptureAtkModule* thisPtr, float delta)
{
// The game clears the AgentUpdateFlag in the original function, but it also updates agents in it too.
// We'll make a copy of the flags, so that we can fire events after the agents have been updated.
var agentUpdateFlag = thisPtr->AgentUpdateFlag;
this.raptureAtkModuleUpdateHook.Original(thisPtr, delta);
if (agentUpdateFlag != RaptureAtkModule.AgentUpdateFlags.None)
{
this.AgentUpdate.InvokeSafely((AgentUpdateFlag)agentUpdateFlag);
}
}
}
/// <summary>
@ -381,6 +402,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
this.gameGuiService.UiHideToggled += this.UiHideToggledForward;
this.gameGuiService.HoveredItemChanged += this.HoveredItemForward;
this.gameGuiService.HoveredActionChanged += this.HoveredActionForward;
this.gameGuiService.AgentUpdate += this.AgentUpdateForward;
}
/// <inheritdoc/>
@ -392,6 +414,9 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public event EventHandler<HoveredAction>? HoveredActionChanged;
/// <inheritdoc/>
public event Action<AgentUpdateFlag> AgentUpdate;
/// <inheritdoc/>
public bool GameUiHidden => this.gameGuiService.GameUiHidden;
@ -411,6 +436,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
this.gameGuiService.UiHideToggled -= this.UiHideToggledForward;
this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward;
this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward;
this.gameGuiService.AgentUpdate -= this.AgentUpdateForward;
this.UiHideToggled = null;
this.HoveredItemChanged = null;
@ -441,6 +467,10 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
public AtkUnitBasePtr GetAddonByName(string name, int index = 1)
=> this.gameGuiService.GetAddonByName(name, index);
/// <inheritdoc/>
public unsafe T* GetAddonByName<T>(string name, int index = 1) where T : unmanaged
=> (T*)this.gameGuiService.GetAddonByName(name, index).Address;
/// <inheritdoc/>
public AgentInterfacePtr GetAgentById(int id)
=> this.gameGuiService.GetAgentById(id);
@ -458,4 +488,6 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId);
private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction);
private void AgentUpdateForward(AgentUpdateFlag agentUpdateFlag) => this.AgentUpdate.InvokeSafely(agentUpdateFlag);
}

View file

@ -340,9 +340,16 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
return null;
}
return this.gameObject ??= this.context.ObjectTable[
this.context.Ui3DModule->NamePlateObjectInfoPointers[this.ArrayIndex]
.Value->GameObject->ObjectIndex];
if (this.ArrayIndex >= this.context.Ui3DModule->NamePlateObjectInfoCount)
return null;
var objectInfoPtr = this.context.Ui3DModule->NamePlateObjectInfoPointers[this.ArrayIndex];
if (objectInfoPtr.Value == null) return null;
var gameObjectPtr = objectInfoPtr.Value->GameObject;
if (gameObjectPtr == null) return null;
return this.gameObject ??= this.context.ObjectTable[gameObjectPtr->ObjectIndex];
}
}

View file

@ -1,12 +1,12 @@
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -113,7 +113,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
// 3 == Close
if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
if (eventType == AtkEventType.InputReceived && WindowSystem.ShouldInhibitAtkCloseEvents && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
{
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return;
@ -124,7 +124,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize)
{
if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled)
if (WindowSystem.ShouldInhibitAtkCloseEvents && this.configuration.IsFocusManagementEnabled)
{
Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return;
@ -185,17 +185,23 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
secondStringEntry->ChangeType(ValueType.String);
const int color = 539;
var strPlugins = new SeString().Append(new UIForegroundPayload(color))
.Append($"{SeIconChar.BoxedLetterD.ToIconString()} ")
.Append(new UIForegroundPayload(0))
.Append(this.locDalamudPlugins).Encode();
var strSettings = new SeString().Append(new UIForegroundPayload(color))
.Append($"{SeIconChar.BoxedLetterD.ToIconString()} ")
.Append(new UIForegroundPayload(0))
.Append(this.locDalamudSettings).Encode();
firstStringEntry->SetManagedString(strPlugins);
secondStringEntry->SetManagedString(strSettings);
using var rssb = new RentedSeStringBuilder();
firstStringEntry->SetManagedString(rssb.Builder
.PushColorType(color)
.Append($"{SeIconChar.BoxedLetterD.ToIconString()} ")
.PopColorType()
.Append(this.locDalamudPlugins)
.GetViewAsSpan());
rssb.Builder.Clear();
secondStringEntry->SetManagedString(rssb.Builder
.PushColorType(color)
.Append($"{SeIconChar.BoxedLetterD.ToIconString()} ")
.PopColorType()
.Append(this.locDalamudSettings)
.GetViewAsSpan());
// open menu with new size
var sizeEntry = &atkValueArgs[4];

View file

@ -114,7 +114,7 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService
this.ResetCompletionData();
this.ClearCachedCommands();
var currentText = component->UnkText1.StringPtr.ExtractText();
var currentText = component->EvaluatedString.StringPtr.ExtractText();
var commands = this.commandManager.Commands
.Where(kv => kv.Value.ShowInHelp && (currentText.Length == 0 || kv.Key.StartsWith(currentText)))
@ -195,7 +195,7 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService
component = (AtkComponentTextInput*)componentBase;
addon = component->ContainingAddon;
addon = component->OwnerAddon;
if (addon == null)
addon = component->ContainingAddon2;

View file

@ -2,16 +2,14 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Gui;
using Dalamud.Game.Inventory.InventoryEventArgTypes;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Game.Inventory;
/// <summary>
@ -33,7 +31,8 @@ internal class GameInventory : IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly Hook<RaptureAtkModuleUpdateDelegate> raptureAtkModuleUpdateHook;
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
private readonly GameInventoryType[] inventoryTypes;
private readonly GameInventoryItem[]?[] inventoryItems;
@ -47,18 +46,9 @@ internal class GameInventory : IInternalDisposableService
this.inventoryTypes = Enum.GetValues<GameInventoryType>();
this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][];
unsafe
{
this.raptureAtkModuleUpdateHook = Hook<RaptureAtkModuleUpdateDelegate>.FromFunctionPointerVariable(
new(&RaptureAtkModule.StaticVirtualTablePointer->Update),
this.RaptureAtkModuleUpdateDetour);
}
this.raptureAtkModuleUpdateHook.Enable();
this.gameGui.AgentUpdate += this.OnAgentUpdate;
}
private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -68,7 +58,7 @@ internal class GameInventory : IInternalDisposableService
this.subscribersPendingChange.Clear();
this.subscribersChanged = false;
this.framework.Update -= this.OnFrameworkUpdate;
this.raptureAtkModuleUpdateHook.Dispose();
this.gameGui.AgentUpdate -= this.OnAgentUpdate;
}
}
@ -319,10 +309,9 @@ internal class GameInventory : IInternalDisposableService
return items;
}
private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1)
private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
{
this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0;
this.raptureAtkModuleUpdateHook.Original(ram, f1);
this.inventoriesMightBeChanged |= true;
}
/// <summary>

View file

@ -11,18 +11,18 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Network.Internal.MarketBoardUploaders;
using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis;
using Dalamud.Game.Network.Structures;
using Dalamud.Game.Player;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Networking.Http;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Network;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.Sheets;
using Serilog;
namespace Dalamud.Game.Network.Internal;
@ -269,29 +269,8 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private static (ulong UploaderId, uint WorldId) GetUploaderInfo()
{
var agentLobby = AgentLobby.Instance();
var uploaderId = agentLobby->LobbyData.ContentId;
if (uploaderId == 0)
{
var playerState = PlayerState.Instance();
if (playerState->IsLoaded == 1)
{
uploaderId = playerState->ContentId;
}
}
var worldId = agentLobby->LobbyData.CurrentWorldId;
if (worldId == 0)
{
var localPlayer = Control.GetLocalPlayer();
if (localPlayer != null)
{
worldId = localPlayer->CurrentWorld;
}
}
return (uploaderId, worldId);
var playerState = Service<PlayerState>.Get();
return (playerState.ContentId, playerState.CurrentWorld.RowId);
}
private unsafe nint CfPopDetour(PublicContentDirector.EnterContentInfoPacket* packetData)

View file

@ -16,6 +16,6 @@ internal class NetworkHandlersAddressResolver : BaseAddressResolver
{
this.CustomTalkEventResponsePacketHandler =
scanner.ScanText(
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 48 8B C8 44 0F B6 CF 0F B6 44 24 ?? 44 0F B7 C6 88 44 24 ?? 8B D5 48 89 5C 24"); // unnamed in CS
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 44 0F B6 54 24 ?? 44 0F B6 CF 44 88 54 24 ?? 44 0F B7 C6 8B D5"); // unnamed in CS
}
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Player;
/// <summary>
/// Specifies the mentor certification version for a player.
/// </summary>
public enum MentorVersion : byte
{
/// <summary>
/// Indicates that the player has never held mentor status in any expansion.
/// </summary>
None = 0,
/// <summary>
/// Indicates that the player was last a mentor during the <c>Shadowbringers</c> expansion.
/// </summary>
Shadowbringers = 1,
/// <summary>
/// Indicates that the player was last a mentor during the <c>Endwalker</c> expansion.
/// </summary>
Endwalker = 2,
/// <summary>
/// Indicates that the player was last a mentor during the <c>Dawntrail</c> expansion.
/// </summary>
Dawntrail = 3,
}

View file

@ -0,0 +1,489 @@
namespace Dalamud.Game.Player;
/// <summary>
/// Represents a player's attribute.
/// </summary>
public enum PlayerAttribute
{
/// <summary>
/// Strength.
/// </summary>
/// <remarks>
/// Affects physical damage dealt by gladiator's arms, marauder's arms, dark knight's arms, gunbreaker's arms, pugilist's arms, lancer's arms, samurai's arms, reaper's arms, thaumaturge's arms, arcanist's arms, red mage's arms, pictomancer's arms, conjurer's arms, astrologian's arms, sage's arms, and blue mage's arms.
/// </remarks>
Strength = 1,
/// <summary>
/// Dexterity.
/// </summary>
/// <remarks>
/// Affects physical damage dealt by rogue's arms, viper's arms, archer's arms, machinist's arms, and dancer's arms.
/// </remarks>
Dexterity = 2,
/// <summary>
/// Vitality.
/// </summary>
/// <remarks>
/// Affects maximum HP.
/// </remarks>
Vitality = 3,
/// <summary>
/// Intelligence.
/// </summary>
/// <remarks>
/// Affects attack magic potency when role is DPS.
/// </remarks>
Intelligence = 4,
/// <summary>
/// Mind.
/// </summary>
/// <remarks>
/// Affects healing magic potency. Also affects attack magic potency when role is Healer.
/// </remarks>
Mind = 5,
/// <summary>
/// Piety.
/// </summary>
/// <remarks>
/// Affects MP regeneration. Regeneration rate is determined by piety. Only applicable when in battle and role is Healer.
/// </remarks>
Piety = 6,
/// <summary>
/// Health Points.
/// </summary>
HP = 7,
/// <summary>
/// Mana Points.
/// </summary>
MP = 8,
/// <summary>
/// Tactical Points.
/// </summary>
TP = 9,
/// <summary>
/// Gathering Point.
/// </summary>
GP = 10,
/// <summary>
/// Crafting Points.
/// </summary>
CP = 11,
/// <summary>
/// Physical Damage.
/// </summary>
PhysicalDamage = 12,
/// <summary>
/// Magic Damage.
/// </summary>
MagicDamage = 13,
/// <summary>
/// Delay.
/// </summary>
Delay = 14,
/// <summary>
/// Additional Effect.
/// </summary>
AdditionalEffect = 15,
/// <summary>
/// Attack Speed.
/// </summary>
AttackSpeed = 16,
/// <summary>
/// Block Rate.
/// </summary>
BlockRate = 17,
/// <summary>
/// Block Strength.
/// </summary>
BlockStrength = 18,
/// <summary>
/// Tenacity.
/// </summary>
/// <remarks>
/// Affects the amount of physical and magic damage dealt and received, as well as HP restored. The higher the value, the more damage dealt, the more HP restored, and the less damage taken. Only applicable when role is Tank.
/// </remarks>
Tenacity = 19,
/// <summary>
/// Attack Power.
/// </summary>
/// <remarks>
/// Affects amount of damage dealt by physical attacks. The higher the value, the more damage dealt.
/// </remarks>
AttackPower = 20,
/// <summary>
/// Defense.
/// </summary>
/// <remarks>
/// Affects the amount of damage taken by physical attacks. The higher the value, the less damage taken.
/// </remarks>
Defense = 21,
/// <summary>
/// Direct Hit Rate.
/// </summary>
/// <remarks>
/// Affects the rate at which your physical and magic attacks land direct hits, dealing slightly more damage than normal hits. The higher the value, the higher the frequency with which your hits will be direct. Higher values will also result in greater damage for actions which guarantee direct hits.
/// </remarks>
DirectHitRate = 22,
/// <summary>
/// Evasion.
/// </summary>
Evasion = 23,
/// <summary>
/// Magic Defense.
/// </summary>
/// <remarks>
/// Affects the amount of damage taken by magic attacks. The higher the value, the less damage taken.
/// </remarks>
MagicDefense = 24,
/// <summary>
/// Critical Hit Power.
/// </summary>
CriticalHitPower = 25,
/// <summary>
/// Critical Hit Resilience.
/// </summary>
CriticalHitResilience = 26,
/// <summary>
/// Critical Hit.
/// </summary>
/// <remarks>
/// Affects the amount of physical and magic damage dealt, as well as HP restored. The higher the value, the higher the frequency with which your hits will be critical/higher the potency of critical hits.
/// </remarks>
CriticalHit = 27,
/// <summary>
/// Critical Hit Evasion.
/// </summary>
CriticalHitEvasion = 28,
/// <summary>
/// Slashing Resistance.
/// </summary>
/// <remarks>
/// Decreases damage done by slashing attacks.
/// </remarks>
SlashingResistance = 29,
/// <summary>
/// Piercing Resistance.
/// </summary>
/// <remarks>
/// Decreases damage done by piercing attacks.
/// </remarks>
PiercingResistance = 30,
/// <summary>
/// Blunt Resistance.
/// </summary>
/// <remarks>
/// Decreases damage done by blunt attacks.
/// </remarks>
BluntResistance = 31,
/// <summary>
/// Projectile Resistance.
/// </summary>
ProjectileResistance = 32,
/// <summary>
/// Attack Magic Potency.
/// </summary>
/// <remarks>
/// Affects the amount of damage dealt by magic attacks.
/// </remarks>
AttackMagicPotency = 33,
/// <summary>
/// Healing Magic Potency.
/// </summary>
/// <remarks>
/// Affects the amount of HP restored via healing magic.
/// </remarks>
HealingMagicPotency = 34,
/// <summary>
/// Enhancement Magic Potency.
/// </summary>
EnhancementMagicPotency = 35,
/// <summary>
/// Elemental Bonus.
/// </summary>
ElementalBonus = 36,
/// <summary>
/// Fire Resistance.
/// </summary>
/// <remarks>
/// Decreases fire-aspected damage.
/// </remarks>
FireResistance = 37,
/// <summary>
/// Ice Resistance.
/// </summary>
/// <remarks>
/// Decreases ice-aspected damage.
/// </remarks>
IceResistance = 38,
/// <summary>
/// Wind Resistance.
/// </summary>
/// <remarks>
/// Decreases wind-aspected damage.
/// </remarks>
WindResistance = 39,
/// <summary>
/// Earth Resistance.
/// </summary>
/// <remarks>
/// Decreases earth-aspected damage.
/// </remarks>
EarthResistance = 40,
/// <summary>
/// Lightning Resistance.
/// </summary>
/// <remarks>
/// Decreases lightning-aspected damage.
/// </remarks>
LightningResistance = 41,
/// <summary>
/// Water Resistance.
/// </summary>
/// <remarks>
/// Decreases water-aspected damage.
/// </remarks>
WaterResistance = 42,
/// <summary>
/// Magic Resistance.
/// </summary>
MagicResistance = 43,
/// <summary>
/// Determination.
/// </summary>
/// <remarks>
/// Affects the amount of damage dealt by both physical and magic attacks, as well as the amount of HP restored by healing spells.
/// </remarks>
Determination = 44,
/// <summary>
/// Skill Speed.
/// </summary>
/// <remarks>
/// Affects both the casting and recast timers, as well as the damage over time potency for weaponskills and auto-attacks. The higher the value, the shorter the timers/higher the potency.
/// </remarks>
SkillSpeed = 45,
/// <summary>
/// Spell Speed.
/// </summary>
/// <remarks>
/// Affects both the casting and recast timers for spells. The higher the value, the shorter the timers. Also affects a spell's damage over time or healing over time potency.
/// </remarks>
SpellSpeed = 46,
/// <summary>
/// Haste.
/// </summary>
Haste = 47,
/// <summary>
/// Morale.
/// </summary>
/// <remarks>
/// In PvP, replaces physical and magical defense in determining damage inflicted by other players. Also influences the amount of damage dealt to other players.
/// </remarks>
Morale = 48,
/// <summary>
/// Enmity.
/// </summary>
Enmity = 49,
/// <summary>
/// Enmity Reduction.
/// </summary>
EnmityReduction = 50,
/// <summary>
/// Desynthesis Skill Gain.
/// </summary>
DesynthesisSkillGain = 51,
/// <summary>
/// EXP Bonus.
/// </summary>
EXPBonus = 52,
/// <summary>
/// Regen.
/// </summary>
Regen = 53,
/// <summary>
/// Special Attribute.
/// </summary>
SpecialAttribute = 54,
/// <summary>
/// Main Attribute.
/// </summary>
MainAttribute = 55,
/// <summary>
/// Secondary Attribute.
/// </summary>
SecondaryAttribute = 56,
/// <summary>
/// Slow Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of slow.
/// </remarks>
SlowResistance = 57,
/// <summary>
/// Petrification Resistance.
/// </summary>
PetrificationResistance = 58,
/// <summary>
/// Paralysis Resistance.
/// </summary>
ParalysisResistance = 59,
/// <summary>
/// Silence Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of silence.
/// </remarks>
SilenceResistance = 60,
/// <summary>
/// Blind Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of blind.
/// </remarks>
BlindResistance = 61,
/// <summary>
/// Poison Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of poison.
/// </remarks>
PoisonResistance = 62,
/// <summary>
/// Stun Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of stun.
/// </remarks>
StunResistance = 63,
/// <summary>
/// Sleep Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of sleep.
/// </remarks>
SleepResistance = 64,
/// <summary>
/// Bind Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of bind.
/// </remarks>
BindResistance = 65,
/// <summary>
/// Heavy Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of heavy.
/// </remarks>
HeavyResistance = 66,
/// <summary>
/// Doom Resistance.
/// </summary>
DoomResistance = 67,
/// <summary>
/// Reduced Durability Loss.
/// </summary>
ReducedDurabilityLoss = 68,
/// <summary>
/// Increased Spiritbond Gain.
/// </summary>
IncreasedSpiritbondGain = 69,
/// <summary>
/// Craftsmanship.
/// </summary>
/// <remarks>
/// Affects the amount of progress achieved in a single synthesis step.
/// </remarks>
Craftsmanship = 70,
/// <summary>
/// Control.
/// </summary>
/// <remarks>
/// Affects the amount of quality improved in a single synthesis step.
/// </remarks>
Control = 71,
/// <summary>
/// Gathering.
/// </summary>
/// <remarks>
/// Affects the rate at which items are gathered.
/// </remarks>
Gathering = 72,
/// <summary>
/// Perception.
/// </summary>
/// <remarks>
/// Affects item yield when gathering as a botanist or miner, and the size of fish when fishing or spearfishing.
/// </remarks>
Perception = 73,
}

View file

@ -0,0 +1,232 @@
using System.Collections.Generic;
using Dalamud.Data;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
using GrandCompany = Lumina.Excel.Sheets.GrandCompany;
namespace Dalamud.Game.Player;
/// <summary>
/// This class contains the PlayerState wrappers.
/// </summary>
[PluginInterface]
[ServiceManager.EarlyLoadedService]
[ResolveVia<IPlayerState>]
internal unsafe class PlayerState : IServiceType, IPlayerState
{
[ServiceManager.ServiceConstructor]
private PlayerState()
{
}
/// <inheritdoc/>
public bool IsLoaded => CSPlayerState.Instance()->IsLoaded;
/// <inheritdoc/>
public string CharacterName => this.IsLoaded ? CSPlayerState.Instance()->CharacterNameString : string.Empty;
/// <inheritdoc/>
public uint EntityId => this.IsLoaded ? CSPlayerState.Instance()->EntityId : default;
/// <inheritdoc/>
public ulong ContentId => this.IsLoaded ? CSPlayerState.Instance()->ContentId : default;
/// <inheritdoc/>
public RowRef<World> CurrentWorld
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby->IsLoggedIn
? LuminaUtils.CreateRef<World>(agentLobby->LobbyData.CurrentWorldId)
: default;
}
}
/// <inheritdoc/>
public RowRef<World> HomeWorld
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby->IsLoggedIn
? LuminaUtils.CreateRef<World>(agentLobby->LobbyData.HomeWorldId)
: default;
}
}
/// <inheritdoc/>
public Sex Sex => this.IsLoaded ? (Sex)CSPlayerState.Instance()->Sex : default;
/// <inheritdoc/>
public RowRef<Race> Race => this.IsLoaded ? LuminaUtils.CreateRef<Race>(CSPlayerState.Instance()->Race) : default;
/// <inheritdoc/>
public RowRef<Tribe> Tribe => this.IsLoaded ? LuminaUtils.CreateRef<Tribe>(CSPlayerState.Instance()->Tribe) : default;
/// <inheritdoc/>
public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default;
/// <inheritdoc/>
public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel;
/// <inheritdoc/>
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;
/// <inheritdoc/>
public short EffectiveLevel => this.IsLoaded ? (this.IsLevelSynced ? CSPlayerState.Instance()->SyncedLevel : CSPlayerState.Instance()->CurrentLevel) : default;
/// <inheritdoc/>
public RowRef<GuardianDeity> GuardianDeity => this.IsLoaded ? LuminaUtils.CreateRef<GuardianDeity>(CSPlayerState.Instance()->GuardianDeity) : default;
/// <inheritdoc/>
public byte BirthMonth => this.IsLoaded ? CSPlayerState.Instance()->BirthMonth : default;
/// <inheritdoc/>
public byte BirthDay => this.IsLoaded ? CSPlayerState.Instance()->BirthDay : default;
/// <inheritdoc/>
public RowRef<ClassJob> FirstClass => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->FirstClass) : default;
/// <inheritdoc/>
public RowRef<Town> StartTown => this.IsLoaded ? LuminaUtils.CreateRef<Town>(CSPlayerState.Instance()->StartTown) : default;
/// <inheritdoc/>
public int BaseStrength => this.IsLoaded ? CSPlayerState.Instance()->BaseStrength : default;
/// <inheritdoc/>
public int BaseDexterity => this.IsLoaded ? CSPlayerState.Instance()->BaseDexterity : default;
/// <inheritdoc/>
public int BaseVitality => this.IsLoaded ? CSPlayerState.Instance()->BaseVitality : default;
/// <inheritdoc/>
public int BaseIntelligence => this.IsLoaded ? CSPlayerState.Instance()->BaseIntelligence : default;
/// <inheritdoc/>
public int BaseMind => this.IsLoaded ? CSPlayerState.Instance()->BaseMind : default;
/// <inheritdoc/>
public int BasePiety => this.IsLoaded ? CSPlayerState.Instance()->BasePiety : default;
/// <inheritdoc/>
public RowRef<GrandCompany> GrandCompany => this.IsLoaded ? LuminaUtils.CreateRef<GrandCompany>(CSPlayerState.Instance()->GrandCompany) : default;
/// <inheritdoc/>
public RowRef<Aetheryte> HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef<Aetheryte>(CSPlayerState.Instance()->HomeAetheryteId) : default;
/// <inheritdoc/>
public IReadOnlyList<RowRef<Aetheryte>> FavoriteAetherytes
{
get
{
var playerState = CSPlayerState.Instance();
if (!playerState->IsLoaded)
return default;
var count = playerState->FavouriteAetheryteCount;
if (count == 0)
return default;
var array = new RowRef<Aetheryte>[count];
for (var i = 0; i < count; i++)
array[i] = LuminaUtils.CreateRef<Aetheryte>(playerState->FavouriteAetherytes[i]);
return array;
}
}
/// <inheritdoc/>
public RowRef<Aetheryte> FreeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef<Aetheryte>(CSPlayerState.Instance()->FreeAetheryteId) : default;
/// <inheritdoc/>
public uint BaseRestedExperience => this.IsLoaded ? CSPlayerState.Instance()->BaseRestedExperience : default;
/// <inheritdoc/>
public short PlayerCommendations => this.IsLoaded ? CSPlayerState.Instance()->PlayerCommendations : default;
/// <inheritdoc/>
public byte DeliveryLevel => this.IsLoaded ? CSPlayerState.Instance()->DeliveryLevel : default;
/// <inheritdoc/>
public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default;
/// <inheritdoc/>
public bool IsMentor => this.IsLoaded && CSPlayerState.Instance()->IsMentor();
/// <inheritdoc/>
public bool IsBattleMentor => this.IsLoaded && CSPlayerState.Instance()->IsBattleMentor();
/// <inheritdoc/>
public bool IsTradeMentor => this.IsLoaded && CSPlayerState.Instance()->IsTradeMentor();
/// <inheritdoc/>
public bool IsNovice => this.IsLoaded && CSPlayerState.Instance()->IsNovice();
/// <inheritdoc/>
public bool IsReturner => this.IsLoaded && CSPlayerState.Instance()->IsReturner();
/// <inheritdoc/>
public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default;
/// <inheritdoc/>
public byte GetGrandCompanyRank(GrandCompany grandCompany)
{
if (!this.IsLoaded)
return default;
return grandCompany.RowId switch
{
1 => CSPlayerState.Instance()->GCRankMaelstrom,
2 => CSPlayerState.Instance()->GCRankTwinAdders,
3 => CSPlayerState.Instance()->GCRankImmortalFlames,
_ => default,
};
}
/// <inheritdoc/>
public short GetClassJobLevel(ClassJob classJob)
{
if (classJob.ExpArrayIndex == -1)
return default;
if (!this.IsLoaded)
return default;
return CSPlayerState.Instance()->ClassJobLevels[classJob.ExpArrayIndex];
}
/// <inheritdoc/>
public int GetClassJobExperience(ClassJob classJob)
{
if (classJob.ExpArrayIndex == -1)
return default;
if (!this.IsLoaded)
return default;
return CSPlayerState.Instance()->ClassJobExperience[classJob.ExpArrayIndex];
}
/// <inheritdoc/>
public float GetDesynthesisLevel(ClassJob classJob)
{
if (classJob.DohDolJobIndex == -1)
return default;
if (!this.IsLoaded)
return default;
return CSPlayerState.Instance()->DesynthesisLevels[classJob.DohDolJobIndex] / 100f;
}
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.Game.Player;
/// <summary>
/// Represents the sex of a character.
/// </summary>
public enum Sex : byte
{
/// <summary>
/// Male sex.
/// </summary>
Male = 0,
/// <summary>
/// Female sex.
/// </summary>
Female = 1,
}

View file

@ -7,6 +7,7 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Iced.Intel;
using Newtonsoft.Json;
using Serilog;
@ -325,7 +326,7 @@ public class SigScanner : IDisposable, ISigScanner
}
/// <inheritdoc/>
public nint[] ScanAllText(string signature) => this.ScanAllText(signature, default).ToArray();
public nint[] ScanAllText(string signature) => this.ScanAllText(signature, CancellationToken.None).ToArray();
/// <inheritdoc/>
public IEnumerable<nint> ScanAllText(string signature, CancellationToken cancellationToken)
@ -337,16 +338,16 @@ public class SigScanner : IDisposable, ISigScanner
{
cancellationToken.ThrowIfCancellationRequested();
var index = IndexOf(mBase, this.TextSectionSize, needle, mask, badShift);
var index = IndexOf(mBase, this.TextSectionSize - (int)(mBase - this.TextSectionBase), needle, mask, badShift);
if (index < 0)
break;
var scanRet = mBase + index;
mBase = scanRet + 1;
if (this.IsCopy)
scanRet -= this.moduleCopyOffset;
yield return scanRet;
mBase = scanRet + 1;
}
}

View file

@ -221,8 +221,8 @@ internal class SheetRedirectResolver : IServiceType
sheetName = nameof(LSheets.AkatsukiNoteString);
colIndex = 0;
if (this.dataManager.Excel.GetSubrowSheet<LSheets.AkatsukiNote>().TryGetRow(rowId, out var row))
rowId = (uint)row[0].Unknown2;
if (this.dataManager.Excel.GetSubrowSheet<LSheets.AkatsukiNote>().TryGetSubrow(rowId, 0, out var row))
rowId = row.ListName.RowId;
break;
}
}

View file

@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Config;
using Dalamud.Game.Player;
using Dalamud.Game.Text.Evaluator.Internal;
using Dalamud.Game.Text.Noun;
using Dalamud.Game.Text.Noun.Enums;
@ -35,7 +36,6 @@ using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using AddonSheet = Lumina.Excel.Sheets.Addon;
using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
using StatusSheet = Lumina.Excel.Sheets.Status;
namespace Dalamud.Game.Text.Evaluator;
@ -68,6 +68,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
[ServiceManager.ServiceDependency]
private readonly SheetRedirectResolver sheetRedirectResolver = Service<SheetRedirectResolver>.Get();
[ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get();
private readonly ConcurrentDictionary<StringCacheKey<ActionKind>, string> actStrCache = [];
private readonly ConcurrentDictionary<StringCacheKey<ObjectKind>, string> objStrCache = [];
@ -121,6 +124,15 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language);
}
/// <inheritdoc/>
public ReadOnlySeString EvaluateMacroString(
ReadOnlySpan<byte> macroString,
Span<SeStringParameter> localParameters = default,
ClientLanguage? language = null)
{
return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language);
}
/// <inheritdoc/>
public ReadOnlySeString EvaluateFromAddon(
uint addonId,
@ -247,6 +259,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
case MacroCode.Switch:
return this.TryResolveSwitch(in context, payload);
case MacroCode.SwitchPlatform:
return this.TryResolveSwitchPlatform(in context, payload);
case MacroCode.PcName:
return this.TryResolvePcName(in context, payload);
@ -315,6 +330,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
case MacroCode.Sheet:
return this.TryResolveSheet(in context, payload);
case MacroCode.SheetSub:
return this.TryResolveSheetSub(in context, payload);
case MacroCode.String:
return this.TryResolveString(in context, payload);
@ -447,6 +465,29 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return false;
}
private bool TryResolveSwitchPlatform(in SeStringContext context, in ReadOnlySePayloadSpan payload)
{
if (!payload.TryGetExpression(out var expr1))
return false;
if (!expr1.TryGetInt(out var intVal))
return false;
// Our version of the game uses IsMacClient() here and the
// Xbox version seems to always return 7 for the platform.
var platform = Util.IsWine() ? 5 : 3;
// The sheet is seeminly split into first 20 rows for wired controllers
// and the last 20 rows for wireless controllers.
var rowId = (uint)((20 * ((intVal - 1) / 20)) + (platform - 4 < 2 ? 2 : 1));
if (!this.dataManager.GetExcelSheet<Platform>().TryGetRow(rowId, out var platformRow))
return false;
context.Builder.Append(platformRow.Name);
return true;
}
private unsafe bool TryResolvePcName(in SeStringContext context, in ReadOnlySePayloadSpan payload)
{
if (!payload.TryGetExpression(out var eEntityId))
@ -526,7 +567,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return false;
// the game uses LocalPlayer here, but using PlayerState seems more safe.
return this.ResolveStringExpression(in context, PlayerState.Instance()->EntityId == entityId ? eTrue : eFalse);
return this.ResolveStringExpression(in context, this.playerState.EntityId == entityId ? eTrue : eFalse);
}
private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -757,6 +798,65 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return true;
}
private bool TryResolveSheetSub(in SeStringContext context, in ReadOnlySePayloadSpan payload)
{
var enu = payload.GetEnumerator();
if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSheetNameStr))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eRowIdValue))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eSubrowIdValue))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eColIndexValue))
return false;
var secondaryRowId = this.GetSubrowSheetIntValue(context.Language, eSheetNameStr.ExtractText(), eRowIdValue, (ushort)eSubrowIdValue, eColIndexValue);
if (secondaryRowId == -1)
return false;
if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSecondarySheetNameStr))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var secondaryColIndex))
return false;
var text = this.FormatSheetValue(context.Language, eSecondarySheetNameStr.ExtractText(), (uint)secondaryRowId, secondaryColIndex, 0);
if (text.IsEmpty)
return false;
this.CreateSheetLink(context, eSecondarySheetNameStr.ExtractText(), text, eRowIdValue, eSubrowIdValue);
return true;
}
private int GetSubrowSheetIntValue(ClientLanguage language, string sheetName, uint rowId, ushort subrowId, uint colIndex)
{
if (!this.dataManager.Excel.SheetNames.Contains(sheetName))
return -1;
if (!this.dataManager.GetSubrowExcelSheet<RawSubrow>(language, sheetName)
.TryGetSubrow(rowId, subrowId, out var row))
return -1;
if (colIndex >= row.Columns.Count)
return -1;
var column = row.Columns[(int)colIndex];
return column.Type switch
{
ExcelColumnDataType.Int8 => row.ReadInt8(column.Offset),
ExcelColumnDataType.UInt8 => row.ReadUInt8(column.Offset),
ExcelColumnDataType.Int16 => row.ReadInt16(column.Offset),
ExcelColumnDataType.UInt16 => row.ReadUInt16(column.Offset),
ExcelColumnDataType.Int32 => row.ReadInt32(column.Offset),
_ => -1,
};
}
private ReadOnlySeString FormatSheetValue(ClientLanguage language, string sheetName, uint rowId, uint colIndex, uint colParam)
{
if (!this.dataManager.Excel.SheetNames.Contains(sheetName))
@ -1532,23 +1632,63 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return true;
var isNoun = false;
var col = 0;
if (ranges.StartsWith("noun"))
{
isNoun = true;
}
else if (ranges.StartsWith("col"))
{
var colRangeEnd = ranges.IndexOf(',');
if (colRangeEnd == -1)
colRangeEnd = ranges.Length;
var colIndex = 0;
Span<int> cols = stackalloc int[8];
cols.Clear();
var hasRanges = false;
var isInRange = false;
col = int.Parse(ranges[4..colRangeEnd]);
}
else if (ranges.StartsWith("tail"))
while (!string.IsNullOrWhiteSpace(ranges))
{
// find the end of the current entry
var entryEnd = ranges.IndexOf(',');
if (entryEnd == -1)
entryEnd = ranges.Length;
var entry = ranges.AsSpan(0, entryEnd);
if (ranges.StartsWith("noun", StringComparison.Ordinal))
{
isNoun = true;
}
else if (ranges.StartsWith("col", StringComparison.Ordinal) && colIndex < cols.Length)
{
cols[colIndex++] = int.Parse(entry[4..]);
}
else if (ranges.StartsWith("tail", StringComparison.Ordinal))
{
// currently not supported, since there are no known uses
context.Builder.Append(payload);
return false;
}
else
{
var dash = entry.IndexOf('-');
hasRanges |= true;
if (dash == -1)
{
isInRange |= int.Parse(entry) == rowId;
}
else
{
isInRange |= rowId >= int.Parse(entry[..dash])
&& rowId <= int.Parse(entry[(dash + 1)..]);
}
}
// if it's the end of the string, we're done
if (entryEnd == ranges.Length)
break;
// else, move to the next entry
ranges = ranges[(entryEnd + 1)..].TrimStart();
}
if (hasRanges && !isInRange)
{
// couldn't find any, so we don't handle them :p
context.Builder.Append(payload);
return false;
}
@ -1566,7 +1706,23 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
}
else if (this.dataManager.GetExcelSheet<RawRow>(context.Language, sheetName).TryGetRow(rowId, out var row))
{
context.Builder.Append(row.ReadStringColumn(col));
if (colIndex == 0)
{
context.Builder.Append(row.ReadStringColumn(0));
return true;
}
else
{
for (var i = 0; i < colIndex; i++)
{
var text = row.ReadStringColumn(cols[i]);
if (!text.IsEmpty)
{
context.Builder.Append(text);
break;
}
}
}
}
return true;
@ -1910,7 +2066,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
value = (uint)MacroDecoder.GetMacroTime()->tm_mday;
return true;
case ExpressionType.Weekday:
value = (uint)MacroDecoder.GetMacroTime()->tm_wday;
value = (uint)MacroDecoder.GetMacroTime()->tm_wday + 1;
return true;
case ExpressionType.Month:
value = (uint)MacroDecoder.GetMacroTime()->tm_mon + 1;

View file

@ -71,9 +71,12 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
public static implicit operator SeStringParameter(string value) => new(value);
public static implicit operator SeStringParameter(ReadOnlySpan<byte> value) => new(value);
}

View file

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

View file

@ -784,4 +784,9 @@ public enum BitmapFontIcon : uint
/// The Cross-World Party Member icon.
/// </summary>
CrossWorldPartyMember = 181,
/// <summary>
/// The Event Tutorial icon.
/// </summary>
EventTutorial = 182,
}

View file

@ -2,11 +2,8 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
using Newtonsoft.Json;
using Serilog;
// TODOs:
@ -117,7 +114,7 @@ public abstract partial class Payload
var chunkType = (SeStringChunkType)reader.ReadByte();
var chunkLen = GetInteger(reader);
var packetStart = reader.BaseStream.Position;
var expressionsStart = reader.BaseStream.Position;
// any unhandled payload types will be turned into a RawPayload with the exact same binary data
switch (chunkType)
@ -208,11 +205,10 @@ public abstract partial class Payload
}
payload ??= new RawPayload((byte)chunkType);
payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen - 1);
payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen);
// read through the rest of the packet
var readBytes = (uint)(reader.BaseStream.Position - packetStart);
reader.ReadBytes((int)(chunkLen - readBytes + 1)); // +1 for the END_BYTE marker
// skip to the end of the payload, in case the specific payload handler didn't read everything
reader.BaseStream.Seek(expressionsStart + chunkLen + 1, SeekOrigin.Begin); // +1 for the END_BYTE marker
return payload;
}

View file

@ -1,14 +1,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.Text.Evaluator;
using Lumina.Excel.Sheets;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -17,7 +14,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class AutoTranslatePayload : Payload, ITextProvider
{
private string? text;
private ReadOnlySeString payload;
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
@ -34,6 +31,14 @@ public class AutoTranslatePayload : Payload, ITextProvider
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
this.Group = group;
this.Key = key;
var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
this.payload = ssb.BeginMacro(MacroCode.Fixed)
.AppendUIntExpression(group - 1)
.AppendUIntExpression(key)
.EndMacro()
.ToReadOnlySeString();
Lumina.Text.SeStringBuilder.SharedPool.Return(ssb);
}
/// <summary>
@ -41,6 +46,7 @@ public class AutoTranslatePayload : Payload, ITextProvider
/// </summary>
internal AutoTranslatePayload()
{
this.payload = default; // parsed by DecodeImpl
}
/// <summary>
@ -68,8 +74,13 @@ public class AutoTranslatePayload : Payload, ITextProvider
{
get
{
// wrap the text in the colored brackets that is uses in-game, since those are not actually part of any of the payloads
return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {this.Resolve()} {(char)SeIconChar.AutoTranslateClose}";
if (this.Group is 100 or 200)
{
return Service<SeStringEvaluator>.Get().Evaluate(this.payload).ToString();
}
// wrap the text in the colored brackets that are used in-game, since those are not actually part of any of the fixed macro payload
return $"{(char)SeIconChar.AutoTranslateOpen} {Service<SeStringEvaluator>.Get().Evaluate(this.payload)} {(char)SeIconChar.AutoTranslateClose}";
}
}
@ -85,95 +96,25 @@ public class AutoTranslatePayload : Payload, ITextProvider
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var keyBytes = MakeInteger(this.Key);
var chunkLen = keyBytes.Length + 2;
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen,
(byte)this.Group,
};
bytes.AddRange(keyBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
return this.payload.Data.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// this seems to always be a bare byte, and not following normal integer encoding
// the values in the table are all <70 so this is presumably ok
this.Group = reader.ReadByte();
var body = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
var rosps = new ReadOnlySePayloadSpan(ReadOnlySePayloadType.Macro, MacroCode.Fixed, body.AsSpan());
this.Key = GetInteger(reader);
}
var span = rosps.EnvelopeByteLength <= 512 ? stackalloc byte[rosps.EnvelopeByteLength] : new byte[rosps.EnvelopeByteLength];
rosps.WriteEnvelopeTo(span);
this.payload = new ReadOnlySeString(span);
private static ReadOnlySeString ResolveTextCommand(TextCommand command)
{
// TextCommands prioritize the `Alias` field, if it not empty
// Example for this is /rangerpose2l which becomes /blackrangerposeb in chat
return !command.Alias.IsEmpty ? command.Alias : command.Command;
}
private string Resolve()
{
string value = null;
var excelModule = Service<DataManager>.Get().Excel;
var completionSheet = excelModule.GetSheet<Completion>();
// try to get the row in the Completion table itself, because this is 'easiest'
// The row may not exist at all (if the Key is for another table), or it could be the wrong row
// (again, if it's meant for another table)
if (completionSheet.GetRowOrDefault(this.Key) is { } completion && completion.Group == this.Group)
if (rosps.TryGetExpression(out var expr1, out var expr2)
&& expr1.TryGetUInt(out var group)
&& expr2.TryGetUInt(out var key))
{
// if the row exists in this table and the group matches, this is actually the correct data
value = completion.Text.ExtractText();
this.Group = group + 1;
this.Key = key;
}
else
{
try
{
// we need to get the linked table and do the lookup there instead
// in this case, there will only be one entry for this group id
var row = completionSheet.First(r => r.Group == this.Group);
// many of the names contain valid id ranges after the table name, but we don't need those
var actualTableName = row.LookupTable.ExtractText().Split('[')[0];
var name = actualTableName switch
{
"Action" => excelModule.GetSheet<Lumina.Excel.Sheets.Action>().GetRow(this.Key).Name,
"ActionComboRoute" => excelModule.GetSheet<ActionComboRoute>().GetRow(this.Key).Name,
"BuddyAction" => excelModule.GetSheet<BuddyAction>().GetRow(this.Key).Name,
"ClassJob" => excelModule.GetSheet<ClassJob>().GetRow(this.Key).Name,
"Companion" => excelModule.GetSheet<Companion>().GetRow(this.Key).Singular,
"CraftAction" => excelModule.GetSheet<CraftAction>().GetRow(this.Key).Name,
"GeneralAction" => excelModule.GetSheet<GeneralAction>().GetRow(this.Key).Name,
"GuardianDeity" => excelModule.GetSheet<GuardianDeity>().GetRow(this.Key).Name,
"MainCommand" => excelModule.GetSheet<MainCommand>().GetRow(this.Key).Name,
"Mount" => excelModule.GetSheet<Mount>().GetRow(this.Key).Singular,
"Pet" => excelModule.GetSheet<Pet>().GetRow(this.Key).Name,
"PetAction" => excelModule.GetSheet<PetAction>().GetRow(this.Key).Name,
"PetMirage" => excelModule.GetSheet<PetMirage>().GetRow(this.Key).Name,
"PlaceName" => excelModule.GetSheet<PlaceName>().GetRow(this.Key).Name,
"Race" => excelModule.GetSheet<Race>().GetRow(this.Key).Masculine,
"TextCommand" => AutoTranslatePayload.ResolveTextCommand(excelModule.GetSheet<TextCommand>().GetRow(this.Key)),
"Tribe" => excelModule.GetSheet<Tribe>().GetRow(this.Key).Masculine,
"Weather" => excelModule.GetSheet<Weather>().GetRow(this.Key).Name,
_ => throw new Exception(actualTableName),
};
value = name.ExtractText();
}
catch (Exception e)
{
Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this.Type} - Group: {this.Group}, Key: {this.Key}");
}
}
return value;
}
}

View file

@ -16,7 +16,7 @@ public class DalamudLinkPayload : Payload
public override PayloadType Type => PayloadType.DalamudLink;
/// <summary>Gets the plugin command ID to be linked.</summary>
public Guid CommandId { get; internal set; }
public uint CommandId { get; internal set; }
/// <summary>Gets an optional extra integer value 1.</summary>
public int Extra1 { get; internal set; }
@ -40,7 +40,7 @@ public class DalamudLinkPayload : Payload
var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
var res = ssb.BeginMacro(MacroCode.Link)
.AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
.AppendStringExpression(this.CommandId.ToString())
.AppendUIntExpression(this.CommandId)
.AppendIntExpression(this.Extra1)
.AppendIntExpression(this.Extra2)
.BeginStringExpression()
@ -72,15 +72,15 @@ public class DalamudLinkPayload : Payload
if (!pluginExpression.TryGetString(out var pluginString))
return;
if (!commandIdExpression.TryGetString(out var commandId))
if (!commandIdExpression.TryGetUInt(out var commandId))
return;
this.Plugin = pluginString.ExtractText();
this.CommandId = Guid.Parse(commandId.ExtractText());
this.CommandId = commandId;
}
else
{
if (!commandIdExpression.TryGetString(out var commandId))
if (!commandIdExpression.TryGetUInt(out var commandId))
return;
if (!extra1Expression.TryGetInt(out var extra1))
@ -102,7 +102,7 @@ public class DalamudLinkPayload : Payload
return;
}
this.CommandId = Guid.Parse(commandId.ExtractText());
this.CommandId = commandId;
this.Extra1 = extra1;
this.Extra2 = extra2;
this.Plugin = extraData[0];

View file

@ -95,14 +95,12 @@ public class RawPayload : Payload
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var chunkLen = this.data.Length + 1;
var bytes = new List<byte>()
{
START_BYTE,
this.chunkType,
(byte)chunkLen,
};
bytes.AddRange(MakeInteger((uint)this.data.Length)); // chunkLen
bytes.AddRange(this.data);
bytes.Add(END_BYTE);
@ -113,6 +111,6 @@ public class RawPayload : Payload
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
}
}

View file

@ -118,6 +118,7 @@ public class SeString
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary>

View file

@ -0,0 +1,95 @@
using Lumina.Excel.Sheets;
namespace Dalamud.Game.UnlockState;
/// <summary>
/// Enum for <see cref="ItemAction.Type"/>.
/// </summary>
internal enum ItemActionType : ushort
{
/// <summary>
/// No item action.
/// </summary>
None = 0,
/// <summary>
/// Unlocks a companion (minion).
/// </summary>
Companion = 853,
/// <summary>
/// Unlocks a chocobo companion barding.
/// </summary>
BuddyEquip = 1013,
/// <summary>
/// Unlocks a mount.
/// </summary>
Mount = 1322,
/// <summary>
/// Unlocks recipes from a crafting recipe book.
/// </summary>
SecretRecipeBook = 2136,
/// <summary>
/// Unlocks various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles).
/// </summary>
UnlockLink = 2633,
/// <summary>
/// Unlocks a Triple Triad Card.
/// </summary>
TripleTriadCard = 3357,
/// <summary>
/// Unlocks gathering nodes of a Folklore Tome.
/// </summary>
FolkloreTome = 4107,
/// <summary>
/// Unlocks an Orchestrion Roll.
/// </summary>
OrchestrionRoll = 25183,
/// <summary>
/// Unlocks portrait designs.
/// </summary>
FramersKit = 29459,
/// <summary>
/// Unlocks Bozjan Field Notes.
/// </summary>
/// <remarks> These are server-side but are cached client-side. </remarks>
FieldNotes = 19743,
/// <summary>
/// Unlocks an Ornament (fashion accessory).
/// </summary>
Ornament = 20086,
/// <summary>
/// Unlocks Glasses.
/// </summary>
Glasses = 37312,
/// <summary>
/// Company Seal Vouchers, which convert the item into Company Seals when used.
/// </summary>
CompanySealVouchers = 41120,
/// <summary>
/// Unlocks Occult Records in Occult Crescent.
/// </summary>
OccultRecords = 43141,
/// <summary>
/// Unlocks Phantom Jobs in Occult Crescent.
/// </summary>
SoulShards = 43142,
/// <summary>
/// Star Contributor Certificate, which grants the Star Contributor status in Cosmic Exploration.
/// </summary>
StarContributorCertificate = 45189,
}

View file

@ -0,0 +1,283 @@
using System.Linq;
using CommunityToolkit.HighPerformance;
using Dalamud.Data;
using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.Interop;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.UnlockState;
/// <summary>
/// Represents recipe-related data for all crafting classes.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class RecipeData : IInternalDisposableService
{
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
[ServiceManager.ServiceDependency]
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
private readonly ushort[] craftTypeLevels;
private readonly byte[] unlockedNoteBookDivisionsCount;
private readonly byte[] unlockedSecretNoteBookDivisionsCount;
private readonly ushort[,] noteBookDivisionIds;
private byte[]? cachedUnlockedSecretRecipeBooks;
private byte[]? cachedUnlockLinks;
private byte[]? cachedCompletedQuests;
/// <summary>
/// Initializes a new instance of the <see cref="RecipeData"/> class.
/// </summary>
[ServiceManager.ServiceConstructor]
public RecipeData()
{
var numCraftTypes = this.dataManager.GetExcelSheet<CraftType>().Count();
var numSecretNotBookDivisions = this.dataManager.GetExcelSheet<NotebookDivision>().Count(row => row.RowId is >= 1000 and < 2000);
this.unlockedNoteBookDivisionsCount = new byte[numCraftTypes];
this.unlockedSecretNoteBookDivisionsCount = new byte[numCraftTypes];
this.noteBookDivisionIds = new ushort[numCraftTypes, numSecretNotBookDivisions];
this.craftTypeLevels = new ushort[numCraftTypes];
this.clientState.Login += this.Update;
this.clientState.Logout += this.OnLogout;
this.clientState.LevelChanged += this.OnlevelChanged;
this.gameGui.AgentUpdate += this.OnAgentUpdate;
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.clientState.Login -= this.Update;
this.clientState.Logout -= this.OnLogout;
this.clientState.LevelChanged -= this.OnlevelChanged;
this.gameGui.AgentUpdate -= this.OnAgentUpdate;
}
/// <summary>
/// Determines whether the specified Recipe is unlocked.
/// </summary>
/// <param name="row">The Recipe row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
public bool IsRecipeUnlocked(Recipe row)
{
// E8 ?? ?? ?? ?? 48 63 76 (2025.09.04)
var division = row.RecipeNotebookList.RowId != 0 && row.RecipeNotebookList.IsValid
? (row.RecipeNotebookList.RowId - 1000) / 8 + 1000
: ((uint)row.RecipeLevelTable.Value.ClassJobLevel - 1) / 5;
// E8 ?? ?? ?? ?? 33 ED 84 C0 75 (2025.09.04)
foreach (var craftTypeRow in this.dataManager.GetExcelSheet<CraftType>())
{
var craftType = (byte)craftTypeRow.RowId;
if (division < this.unlockedNoteBookDivisionsCount[craftType])
return true;
if (this.unlockedNoteBookDivisionsCount[craftType] == 0)
continue;
if (division is 5000 or 5001)
return true;
if (division < 1000)
continue;
if (this.unlockedSecretNoteBookDivisionsCount[craftType] == 0)
continue;
if (this.noteBookDivisionIds.GetRowSpan(craftType).Contains((ushort)division))
return true;
}
return false;
}
private void OnLogout(int type, int code)
{
this.cachedUnlockedSecretRecipeBooks = null;
this.cachedUnlockLinks = null;
this.cachedCompletedQuests = null;
}
private void OnlevelChanged(uint classJobId, uint level)
{
if (this.dataManager.GetExcelSheet<ClassJob>().TryGetRow(classJobId, out var classJobRow) &&
classJobRow.ClassJobCategory.RowId == 33) // Crafter
{
this.Update();
}
}
private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
{
if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate))
this.Update();
}
private void Update()
{
// based on Client::Game::UI::RecipeNote.InitializeStructs
if (!this.clientState.IsLoggedIn || !this.NeedsUpdate())
return;
Array.Clear(this.unlockedNoteBookDivisionsCount, 0, this.unlockedNoteBookDivisionsCount.Length);
Array.Clear(this.unlockedSecretNoteBookDivisionsCount, 0, this.unlockedSecretNoteBookDivisionsCount.Length);
Array.Clear(this.noteBookDivisionIds, 0, this.noteBookDivisionIds.Length);
foreach (var craftTypeRow in this.dataManager.GetExcelSheet<CraftType>())
{
var craftType = (byte)craftTypeRow.RowId;
var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType);
if (craftTypeLevel == 0)
continue;
var noteBookDivisionIndex = -1;
foreach (var noteBookDivisionRow in this.dataManager.GetExcelSheet<NotebookDivision>())
{
if (noteBookDivisionRow.RowId < 1000)
{
if (craftTypeLevel >= noteBookDivisionRow.CraftOpeningLevel)
this.unlockedNoteBookDivisionsCount[craftType]++;
}
else if (noteBookDivisionRow.RowId < 2000)
{
noteBookDivisionIndex++;
// For future Lumina.Excel update, replace with:
// if (!notebookDivisionRow.AllowedCraftTypes[craftType])
// continue;
switch (craftTypeRow.RowId)
{
case 0 when !noteBookDivisionRow.CRPCraft: continue;
case 1 when !noteBookDivisionRow.BSMCraft: continue;
case 2 when !noteBookDivisionRow.ARMCraft: continue;
case 3 when !noteBookDivisionRow.GSMCraft: continue;
case 4 when !noteBookDivisionRow.LTWCraft: continue;
case 5 when !noteBookDivisionRow.WVRCraft: continue;
case 6 when !noteBookDivisionRow.ALCCraft: continue;
case 7 when !noteBookDivisionRow.CULCraft: continue;
}
if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue)
continue;
// For future Lumina.Excel update, replace with:
// if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock)
if (noteBookDivisionRow.Unknown1)
{
var secretRecipeBookUnlocked = false;
// For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups
for (var i = 0; i < 2; i++)
{
// For future Lumina.Excel update, replace with:
// if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid)
// continue;
var secretRecipeBookGroupRowId = i switch
{
0 => noteBookDivisionRow.Unknown2,
1 => noteBookDivisionRow.Unknown2,
_ => default,
};
if (secretRecipeBookGroupRowId == 0)
continue;
if (!this.dataManager.GetExcelSheet<SecretRecipeBookGroup>().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow))
continue;
// For future Lumina.Excel update, replace with:
// var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType];
var bitIndex = craftType switch
{
0 => secretRecipeBookGroupRow.Unknown0,
1 => secretRecipeBookGroupRow.Unknown1,
2 => secretRecipeBookGroupRow.Unknown2,
3 => secretRecipeBookGroupRow.Unknown3,
4 => secretRecipeBookGroupRow.Unknown4,
5 => secretRecipeBookGroupRow.Unknown5,
6 => secretRecipeBookGroupRow.Unknown6,
7 => secretRecipeBookGroupRow.Unknown7,
_ => default,
};
if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex))
{
secretRecipeBookUnlocked = true;
break;
}
}
if (noteBookDivisionRow.CraftOpeningLevel > craftTypeLevel && !secretRecipeBookUnlocked)
continue;
}
else if (craftTypeLevel < noteBookDivisionRow.CraftOpeningLevel)
{
continue;
}
else if (noteBookDivisionRow.QuestUnlock.RowId != 0 && !UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(noteBookDivisionRow.QuestUnlock.RowId))
{
continue;
}
this.unlockedSecretNoteBookDivisionsCount[craftType]++;
this.noteBookDivisionIds[craftType, noteBookDivisionIndex] = (ushort)noteBookDivisionRow.RowId;
}
}
}
}
private bool NeedsUpdate()
{
var changed = false;
foreach (var craftTypeRow in this.dataManager.GetExcelSheet<CraftType>())
{
var craftType = (byte)craftTypeRow.RowId;
var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType);
if (this.craftTypeLevels[craftType] != craftTypeLevel)
{
this.craftTypeLevels[craftType] = craftTypeLevel;
changed |= true;
}
}
if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooks.SequenceEqual(this.cachedUnlockedSecretRecipeBooks))
{
this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooks.ToArray();
changed |= true;
}
if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinks.SequenceEqual(this.cachedUnlockLinks))
{
this.cachedUnlockLinks = UIState.Instance()->UnlockLinks.ToArray();
changed |= true;
}
if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuests.SequenceEqual(this.cachedCompletedQuests))
{
this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuests.ToArray();
changed |= true;
}
return changed;
}
}

View file

@ -0,0 +1,858 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using Dalamud.Data;
using Dalamud.Game.Gui;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Component.Exd;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using ActionSheet = Lumina.Excel.Sheets.Action;
using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent;
using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
namespace Dalamud.Game.UnlockState;
#pragma warning disable Dalamud001
/// <summary>
/// This class provides unlock state of various content in the game.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
{
private static readonly ModuleLog Log = new(nameof(UnlockState));
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
[ServiceManager.ServiceDependency]
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly RecipeData recipeData = Service<RecipeData>.Get();
[ServiceManager.ServiceConstructor]
private UnlockState()
{
this.clientState.Login += this.OnLogin;
this.clientState.Logout += this.OnLogout;
this.gameGui.AgentUpdate += this.OnAgentUpdate;
}
/// <inheritdoc/>
public event IUnlockState.UnlockDelegate Unlock;
private bool IsLoaded => PlayerState.Instance()->IsLoaded;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.clientState.Login -= this.OnLogin;
this.clientState.Logout -= this.OnLogout;
this.gameGui.AgentUpdate -= this.OnAgentUpdate;
}
/// <inheritdoc/>
public bool IsActionUnlocked(ActionSheet row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId);
}
/// <inheritdoc/>
public bool IsAetherCurrentUnlocked(AetherCurrent row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsAetherCurrentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsAetherCurrentZoneComplete(row.RowId);
}
/// <inheritdoc/>
public bool IsAozActionUnlocked(AozAction row)
{
if (!this.IsLoaded)
return false;
if (row.RowId == 0 || !row.Action.IsValid)
return false;
return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(row.Action.Value.UnlockLink.RowId);
}
/// <inheritdoc/>
public bool IsBannerBgUnlocked(BannerBg row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerConditionUnlocked(BannerCondition row)
{
if (row.RowId == 0)
return false;
if (!this.IsLoaded)
return false;
var rowPtr = ExdModule.GetBannerConditionByIndex(row.RowId);
if (rowPtr == null)
return false;
return ExdModule.GetBannerConditionUnlockState(rowPtr) == 0;
}
/// <inheritdoc/>
public bool IsBannerDecorationUnlocked(BannerDecoration row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerFacialUnlocked(BannerFacial row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerFrameUnlocked(BannerFrame row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerTimelineUnlocked(BannerTimeline row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBuddyActionUnlocked(BuddyAction row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsBuddyEquipUnlocked(BuddyEquip row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row)
{
return row.IsPurchasable && this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsCompanionUnlocked(Companion row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsCompanionUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsCraftActionUnlocked(CraftAction row)
{
return this.IsUnlockLinkUnlocked(row.QuestRequirement.RowId);
}
/// <inheritdoc/>
public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsEmoteUnlocked(Emote row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row)
{
return this.IsUnlockLinkUnlocked(row.Unknown26);
}
/// <inheritdoc/>
public bool IsEmjCostumeUnlocked(EmjCostume row)
{
return this.dataManager.GetExcelSheet<EmjVoiceNpc>().TryGetRow(row.RowId, out var emjVoiceNpcRow)
&& this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow)
&& QuestManager.IsQuestComplete(row.Unknown1);
}
/// <inheritdoc/>
public bool IsGeneralActionUnlocked(GeneralAction row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsGlassesUnlocked(Glasses row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.RowId);
}
/// <inheritdoc/>
public bool IsHowToUnlocked(HowTo row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsHowToUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsInstanceContentUnlocked(InstanceContentSheet row)
{
if (!this.IsLoaded)
return false;
return UIState.IsInstanceContentUnlocked(row.RowId);
}
/// <inheritdoc/>
public unsafe bool IsItemUnlocked(Item row)
{
if (row.ItemAction.RowId == 0)
return false;
if (!this.IsLoaded)
return false;
// To avoid the ExdModule.GetItemRowById call, which can return null if the excel page
// is not loaded, we're going to imitate the IsItemActionUnlocked call first:
switch ((ItemActionType)row.ItemAction.Value.Type)
{
case ItemActionType.Companion:
return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.BuddyEquip:
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.Mount:
return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.SecretRecipeBook:
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.UnlockLink:
case ItemActionType.OccultRecords:
return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.TripleTriadCard when row.AdditionalData.Is<TripleTriadCard>():
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId);
case ItemActionType.FolkloreTome:
return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.OrchestrionRoll when row.AdditionalData.Is<Orchestrion>():
return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId);
case ItemActionType.FramersKit:
return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId);
case ItemActionType.Ornament:
return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionType.Glasses:
return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId);
case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null:
var supportJobId = (byte)row.ItemAction.Value.Data[0];
return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0;
case ItemActionType.CompanySealVouchers:
return false;
}
var nativeRow = ExdModule.GetItemRowById(row.RowId);
return nativeRow != null && UIState.Instance()->IsItemActionUnlocked(nativeRow) == 1;
}
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row)
{
return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsMJILandmarkUnlocked(MJILandmark row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsMKDLoreUnlocked(MKDLore row)
{
return this.IsUnlockLinkUnlocked(row.Unknown2);
}
/// <inheritdoc/>
public bool IsMountUnlocked(Mount row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsMountUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsNotebookDivisionUnlocked(NotebookDivision row)
{
return this.IsUnlockLinkUnlocked(row.QuestUnlock.RowId);
}
/// <inheritdoc/>
public bool IsOrchestrionUnlocked(Orchestrion row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsOrnamentUnlocked(Ornament row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsOrnamentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsPerformUnlocked(Perform row)
{
return this.IsUnlockLinkUnlocked((uint)row.UnlockLink);
}
/// <inheritdoc/>
public bool IsPublicContentUnlocked(PublicContentSheet row)
{
if (!this.IsLoaded)
return false;
return UIState.IsPublicContentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsRecipeUnlocked(Recipe row)
{
return this.recipeData.IsRecipeUnlocked(row);
}
/// <inheritdoc/>
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsTraitUnlocked(Trait row)
{
return this.IsUnlockLinkUnlocked(row.Quest.RowId);
}
/// <inheritdoc/>
public bool IsTripleTriadCardUnlocked(TripleTriadCard row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.RowId);
}
/// <inheritdoc/>
public bool IsItemUnlockable(Item row)
{
if (row.ItemAction.RowId == 0)
return false;
return (ItemActionType)row.ItemAction.Value.Type
is ItemActionType.Companion
or ItemActionType.BuddyEquip
or ItemActionType.Mount
or ItemActionType.SecretRecipeBook
or ItemActionType.UnlockLink
or ItemActionType.TripleTriadCard
or ItemActionType.FolkloreTome
or ItemActionType.OrchestrionRoll
or ItemActionType.FramersKit
or ItemActionType.Ornament
or ItemActionType.Glasses
or ItemActionType.OccultRecords
or ItemActionType.SoulShards;
}
/// <inheritdoc/>
public bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T>
{
return this.IsRowRefUnlocked((RowRef)rowRef);
}
/// <inheritdoc/>
public bool IsRowRefUnlocked(RowRef rowRef)
{
if (!this.IsLoaded || rowRef.IsUntyped)
return false;
if (rowRef.TryGetValue<ActionSheet>(out var actionRow))
return this.IsActionUnlocked(actionRow);
if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow))
return this.IsAetherCurrentUnlocked(aetherCurrentRow);
if (rowRef.TryGetValue<AetherCurrentCompFlgSet>(out var aetherCurrentCompFlgSetRow))
return this.IsAetherCurrentCompFlgSetUnlocked(aetherCurrentCompFlgSetRow);
if (rowRef.TryGetValue<AozAction>(out var aozActionRow))
return this.IsAozActionUnlocked(aozActionRow);
if (rowRef.TryGetValue<BannerBg>(out var bannerBgRow))
return this.IsBannerBgUnlocked(bannerBgRow);
if (rowRef.TryGetValue<BannerCondition>(out var bannerConditionRow))
return this.IsBannerConditionUnlocked(bannerConditionRow);
if (rowRef.TryGetValue<BannerDecoration>(out var bannerDecorationRow))
return this.IsBannerDecorationUnlocked(bannerDecorationRow);
if (rowRef.TryGetValue<BannerFacial>(out var bannerFacialRow))
return this.IsBannerFacialUnlocked(bannerFacialRow);
if (rowRef.TryGetValue<BannerFrame>(out var bannerFrameRow))
return this.IsBannerFrameUnlocked(bannerFrameRow);
if (rowRef.TryGetValue<BannerTimeline>(out var bannerTimelineRow))
return this.IsBannerTimelineUnlocked(bannerTimelineRow);
if (rowRef.TryGetValue<BuddyAction>(out var buddyActionRow))
return this.IsBuddyActionUnlocked(buddyActionRow);
if (rowRef.TryGetValue<BuddyEquip>(out var buddyEquipRow))
return this.IsBuddyEquipUnlocked(buddyEquipRow);
if (rowRef.TryGetValue<CSBonusContentType>(out var csBonusContentTypeRow))
return this.IsCSBonusContentTypeUnlocked(csBonusContentTypeRow);
if (rowRef.TryGetValue<CharaMakeCustomize>(out var charaMakeCustomizeRow))
return this.IsCharaMakeCustomizeUnlocked(charaMakeCustomizeRow);
if (rowRef.TryGetValue<ChocoboTaxiStand>(out var chocoboTaxiStandRow))
return this.IsChocoboTaxiStandUnlocked(chocoboTaxiStandRow);
if (rowRef.TryGetValue<Companion>(out var companionRow))
return this.IsCompanionUnlocked(companionRow);
if (rowRef.TryGetValue<CraftAction>(out var craftActionRow))
return this.IsCraftActionUnlocked(craftActionRow);
if (rowRef.TryGetValue<Emote>(out var emoteRow))
return this.IsEmoteUnlocked(emoteRow);
if (rowRef.TryGetValue<GeneralAction>(out var generalActionRow))
return this.IsGeneralActionUnlocked(generalActionRow);
if (rowRef.TryGetValue<Glasses>(out var glassesRow))
return this.IsGlassesUnlocked(glassesRow);
if (rowRef.TryGetValue<HowTo>(out var howToRow))
return this.IsHowToUnlocked(howToRow);
if (rowRef.TryGetValue<InstanceContentSheet>(out var instanceContentRow))
return this.IsInstanceContentUnlocked(instanceContentRow);
if (rowRef.TryGetValue<Item>(out var itemRow))
return this.IsItemUnlocked(itemRow);
if (rowRef.TryGetValue<MJILandmark>(out var mjiLandmarkRow))
return this.IsMJILandmarkUnlocked(mjiLandmarkRow);
if (rowRef.TryGetValue<MKDLore>(out var mkdLoreRow))
return this.IsMKDLoreUnlocked(mkdLoreRow);
if (rowRef.TryGetValue<McGuffin>(out var mcGuffinRow))
return this.IsMcGuffinUnlocked(mcGuffinRow);
if (rowRef.TryGetValue<Mount>(out var mountRow))
return this.IsMountUnlocked(mountRow);
if (rowRef.TryGetValue<NotebookDivision>(out var notebookDivisionRow))
return this.IsNotebookDivisionUnlocked(notebookDivisionRow);
if (rowRef.TryGetValue<Orchestrion>(out var orchestrionRow))
return this.IsOrchestrionUnlocked(orchestrionRow);
if (rowRef.TryGetValue<Ornament>(out var ornamentRow))
return this.IsOrnamentUnlocked(ornamentRow);
if (rowRef.TryGetValue<Perform>(out var performRow))
return this.IsPerformUnlocked(performRow);
if (rowRef.TryGetValue<PublicContentSheet>(out var publicContentRow))
return this.IsPublicContentUnlocked(publicContentRow);
if (rowRef.TryGetValue<Recipe>(out var recipeRow))
return this.IsRecipeUnlocked(recipeRow);
if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow))
return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow);
if (rowRef.TryGetValue<Trait>(out var traitRow))
return this.IsTraitUnlocked(traitRow);
if (rowRef.TryGetValue<TripleTriadCard>(out var tripleTriadCardRow))
return this.IsTripleTriadCardUnlocked(tripleTriadCardRow);
return false;
}
/// <inheritdoc/>
public bool IsUnlockLinkUnlocked(ushort unlockLink)
{
if (!this.IsLoaded)
return false;
if (unlockLink == 0)
return false;
return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink);
}
/// <inheritdoc/>
public bool IsUnlockLinkUnlocked(uint unlockLink)
{
if (!this.IsLoaded)
return false;
if (unlockLink == 0)
return false;
return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink);
}
private void OnLogin()
{
this.Update();
}
private void OnLogout(int type, int code)
{
this.cachedUnlockedRowIds.Clear();
}
private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
{
if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate))
this.Update();
}
private void Update()
{
if (!this.IsLoaded)
return;
this.UpdateUnlocksForSheet<ActionSheet>();
this.UpdateUnlocksForSheet<AetherCurrent>();
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>();
this.UpdateUnlocksForSheet<AozAction>();
this.UpdateUnlocksForSheet<BannerBg>();
this.UpdateUnlocksForSheet<BannerCondition>();
this.UpdateUnlocksForSheet<BannerDecoration>();
this.UpdateUnlocksForSheet<BannerFacial>();
this.UpdateUnlocksForSheet<BannerFrame>();
this.UpdateUnlocksForSheet<BannerTimeline>();
this.UpdateUnlocksForSheet<BuddyAction>();
this.UpdateUnlocksForSheet<BuddyEquip>();
this.UpdateUnlocksForSheet<CSBonusContentType>();
this.UpdateUnlocksForSheet<CharaMakeCustomize>();
this.UpdateUnlocksForSheet<ChocoboTaxi>();
this.UpdateUnlocksForSheet<Companion>();
this.UpdateUnlocksForSheet<CraftAction>();
this.UpdateUnlocksForSheet<EmjVoiceNpc>();
this.UpdateUnlocksForSheet<Emote>();
this.UpdateUnlocksForSheet<GeneralAction>();
this.UpdateUnlocksForSheet<Glasses>();
this.UpdateUnlocksForSheet<HowTo>();
this.UpdateUnlocksForSheet<InstanceContentSheet>();
this.UpdateUnlocksForSheet<Item>();
this.UpdateUnlocksForSheet<MJILandmark>();
this.UpdateUnlocksForSheet<MKDLore>();
this.UpdateUnlocksForSheet<McGuffin>();
this.UpdateUnlocksForSheet<Mount>();
this.UpdateUnlocksForSheet<NotebookDivision>();
this.UpdateUnlocksForSheet<Orchestrion>();
this.UpdateUnlocksForSheet<Ornament>();
this.UpdateUnlocksForSheet<Perform>();
this.UpdateUnlocksForSheet<PublicContentSheet>();
this.UpdateUnlocksForSheet<Recipe>();
this.UpdateUnlocksForSheet<SecretRecipeBook>();
this.UpdateUnlocksForSheet<Trait>();
this.UpdateUnlocksForSheet<TripleTriadCard>();
// Not implemented:
// - DescriptionPage: quite complex
// - QuestAcceptAdditionCondition: ignored
// For some other day:
// - FishingSpot
// - Spearfishing
// - Adventure (Sightseeing)
// - MinerFolkloreTome
// - BotanistFolkloreTome
// - FishingFolkloreTome
// - VVD or is that unlocked via quest?
// - VVDNotebookContents?
// - FramersKit (is that just an Item?)
// - ... more?
// Subrow sheets, which are incompatible with the current Unlock event, since RowRef doesn't carry the SubrowId:
// - EmjCostume
// Probably not happening, because it requires fetching data from server:
// - Achievements
// - Titles
// - Bozjan Field Notes
// - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0
}
private void UpdateUnlocksForSheet<T>() where T : struct, IExcelRow<T>
{
var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []);
foreach (var row in this.dataManager.GetExcelSheet<T>())
{
if (unlockedRowIds.Contains(row.RowId))
continue;
var rowRef = LuminaUtils.CreateRef<T>(row.RowId);
if (!this.IsRowRefUnlocked(rowRef))
continue;
unlockedRowIds.Add(row.RowId);
Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}");
foreach (var action in Delegate.EnumerateInvocationList(this.Unlock))
{
try
{
action((RowRef)rowRef);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
}
}
}
/// <summary>
/// Plugin-scoped version of a <see cref="UnlockState"/> service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IUnlockState>]
#pragma warning restore SA1015
internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockState
{
[ServiceManager.ServiceDependency]
private readonly UnlockState unlockStateService = Service<UnlockState>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="UnlockStatePluginScoped"/> class.
/// </summary>
internal UnlockStatePluginScoped()
{
this.unlockStateService.Unlock += this.UnlockForward;
}
/// <inheritdoc/>
public event IUnlockState.UnlockDelegate? Unlock;
/// <inheritdoc/>
public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row);
/// <inheritdoc/>
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row);
/// <inheritdoc/>
public bool IsAetherCurrentUnlocked(AetherCurrent row) => this.unlockStateService.IsAetherCurrentUnlocked(row);
/// <inheritdoc/>
public bool IsAozActionUnlocked(AozAction row) => this.unlockStateService.IsAozActionUnlocked(row);
/// <inheritdoc/>
public bool IsBannerBgUnlocked(BannerBg row) => this.unlockStateService.IsBannerBgUnlocked(row);
/// <inheritdoc/>
public bool IsBannerConditionUnlocked(BannerCondition row) => this.unlockStateService.IsBannerConditionUnlocked(row);
/// <inheritdoc/>
public bool IsBannerDecorationUnlocked(BannerDecoration row) => this.unlockStateService.IsBannerDecorationUnlocked(row);
/// <inheritdoc/>
public bool IsBannerFacialUnlocked(BannerFacial row) => this.unlockStateService.IsBannerFacialUnlocked(row);
/// <inheritdoc/>
public bool IsBannerFrameUnlocked(BannerFrame row) => this.unlockStateService.IsBannerFrameUnlocked(row);
/// <inheritdoc/>
public bool IsBannerTimelineUnlocked(BannerTimeline row) => this.unlockStateService.IsBannerTimelineUnlocked(row);
/// <inheritdoc/>
public bool IsBuddyActionUnlocked(BuddyAction row) => this.unlockStateService.IsBuddyActionUnlocked(row);
/// <inheritdoc/>
public bool IsBuddyEquipUnlocked(BuddyEquip row) => this.unlockStateService.IsBuddyEquipUnlocked(row);
/// <inheritdoc/>
public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) => this.unlockStateService.IsCharaMakeCustomizeUnlocked(row);
/// <inheritdoc/>
public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) => this.unlockStateService.IsChocoboTaxiStandUnlocked(row);
/// <inheritdoc/>
public bool IsCompanionUnlocked(Companion row) => this.unlockStateService.IsCompanionUnlocked(row);
/// <inheritdoc/>
public bool IsCraftActionUnlocked(CraftAction row) => this.unlockStateService.IsCraftActionUnlocked(row);
/// <inheritdoc/>
public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row) => this.unlockStateService.IsCSBonusContentTypeUnlocked(row);
/// <inheritdoc/>
public bool IsEmoteUnlocked(Emote row) => this.unlockStateService.IsEmoteUnlocked(row);
/// <inheritdoc/>
public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) => this.unlockStateService.IsEmjVoiceNpcUnlocked(row);
/// <inheritdoc/>
public bool IsEmjCostumeUnlocked(EmjCostume row) => this.unlockStateService.IsEmjCostumeUnlocked(row);
/// <inheritdoc/>
public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row);
/// <inheritdoc/>
public bool IsGlassesUnlocked(Glasses row) => this.unlockStateService.IsGlassesUnlocked(row);
/// <inheritdoc/>
public bool IsHowToUnlocked(HowTo row) => this.unlockStateService.IsHowToUnlocked(row);
/// <inheritdoc/>
public bool IsInstanceContentUnlocked(InstanceContentSheet row) => this.unlockStateService.IsInstanceContentUnlocked(row);
/// <inheritdoc/>
public bool IsItemUnlockable(Item row) => this.unlockStateService.IsItemUnlockable(row);
/// <inheritdoc/>
public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row);
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row);
/// <inheritdoc/>
public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row);
/// <inheritdoc/>
public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row);
/// <inheritdoc/>
public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row);
/// <inheritdoc/>
public bool IsNotebookDivisionUnlocked(NotebookDivision row) => this.unlockStateService.IsNotebookDivisionUnlocked(row);
/// <inheritdoc/>
public bool IsOrchestrionUnlocked(Orchestrion row) => this.unlockStateService.IsOrchestrionUnlocked(row);
/// <inheritdoc/>
public bool IsOrnamentUnlocked(Ornament row) => this.unlockStateService.IsOrnamentUnlocked(row);
/// <inheritdoc/>
public bool IsPerformUnlocked(Perform row) => this.unlockStateService.IsPerformUnlocked(row);
/// <inheritdoc/>
public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row);
/// <inheritdoc/>
public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row);
/// <inheritdoc/>
public bool IsRowRefUnlocked(RowRef rowRef) => this.unlockStateService.IsRowRefUnlocked(rowRef);
/// <inheritdoc/>
public bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T> => this.unlockStateService.IsRowRefUnlocked(rowRef);
/// <inheritdoc/>
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row);
/// <inheritdoc/>
public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row);
/// <inheritdoc/>
public bool IsTripleTriadCardUnlocked(TripleTriadCard row) => this.unlockStateService.IsTripleTriadCardUnlocked(row);
/// <inheritdoc/>
public bool IsUnlockLinkUnlocked(uint unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink);
/// <inheritdoc/>
public bool IsUnlockLinkUnlocked(ushort unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.unlockStateService.Unlock -= this.UnlockForward;
}
private void UnlockForward(RowRef rowRef) => this.Unlock?.Invoke(rowRef);
}

View file

@ -0,0 +1,4 @@
namespace Dalamud.Interface.ImGuiBackend.Delegates;
/// <summary>Delegate to be called when ImGui should be used to layout now.</summary>
public delegate void ImGuiBuildUiDelegate();

View file

@ -0,0 +1,4 @@
namespace Dalamud.Interface.ImGuiBackend.Delegates;
/// <summary>Delegate to be called on new input frame.</summary>
public delegate void ImGuiNewInputFrameDelegate();

View file

@ -0,0 +1,4 @@
namespace Dalamud.Interface.ImGuiBackend.Delegates;
/// <summary>Delegate to be called on new render frame.</summary>
public delegate void ImGuiNewRenderFrameDelegate();

View file

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGuizmo;
using Dalamud.Bindings.ImPlot;
using Dalamud.Interface.ImGuiBackend.Delegates;
using Dalamud.Interface.ImGuiBackend.Helpers;
using Dalamud.Interface.ImGuiBackend.InputHandler;
using Dalamud.Interface.ImGuiBackend.Renderers;
@ -93,13 +94,13 @@ internal sealed unsafe class Dx11Win32Backend : IWin32Backend
~Dx11Win32Backend() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
public event IImGuiBackend.BuildUiDelegate? BuildUi;
public event ImGuiBuildUiDelegate? BuildUi;
/// <inheritdoc/>
public event IImGuiBackend.NewInputFrameDelegate? NewInputFrame;
public event ImGuiNewInputFrameDelegate? NewInputFrame;
/// <inheritdoc/>
public event IImGuiBackend.NewRenderFrameDelegate? NewRenderFrame;
public event ImGuiNewRenderFrameDelegate? NewRenderFrame;
/// <inheritdoc/>
public bool UpdateCursor

View file

@ -1,4 +1,5 @@
using Dalamud.Interface.ImGuiBackend.InputHandler;
using Dalamud.Interface.ImGuiBackend.Delegates;
using Dalamud.Interface.ImGuiBackend.InputHandler;
using Dalamud.Interface.ImGuiBackend.Renderers;
namespace Dalamud.Interface.ImGuiBackend;
@ -6,23 +7,14 @@ namespace Dalamud.Interface.ImGuiBackend;
/// <summary>Backend for ImGui.</summary>
internal interface IImGuiBackend : IDisposable
{
/// <summary>Delegate to be called when ImGui should be used to layout now.</summary>
public delegate void BuildUiDelegate();
/// <summary>Delegate to be called on new input frame.</summary>
public delegate void NewInputFrameDelegate();
/// <summary>Delegaet to be called on new render frame.</summary>
public delegate void NewRenderFrameDelegate();
/// <summary>User methods invoked every ImGui frame to construct custom UIs.</summary>
event BuildUiDelegate? BuildUi;
event ImGuiBuildUiDelegate? BuildUi;
/// <summary>User methods invoked every ImGui frame on handling inputs.</summary>
event NewInputFrameDelegate? NewInputFrame;
event ImGuiNewInputFrameDelegate? NewInputFrame;
/// <summary>User methods invoked every ImGui frame on handling renders.</summary>
event NewRenderFrameDelegate? NewRenderFrame;
event ImGuiNewRenderFrameDelegate? NewRenderFrame;
/// <summary>Gets or sets a value indicating whether the cursor should be overridden with the ImGui cursor.
/// </summary>
@ -36,7 +28,7 @@ internal interface IImGuiBackend : IDisposable
/// <summary>Gets the input handler.</summary>
IImGuiInputHandler InputHandler { get; }
/// <summary>Gets the renderer.</summary>
IImGuiRenderer Renderer { get; }
@ -45,7 +37,7 @@ internal interface IImGuiBackend : IDisposable
/// <summary>Handles stuff before resizing happens.</summary>
void OnPreResize();
/// <summary>Handles stuff after resizing happens.</summary>
/// <param name="newWidth">The new width.</param>
/// <param name="newHeight">The new height.</param>

View file

@ -1,19 +1,18 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Memory;
using Serilog;
using TerraFX.Interop.Windows;
using static Dalamud.Interface.ImGuiBackend.Helpers.ImGuiViewportHelpers;
using static TerraFX.Interop.Windows.Windows;
using ERROR = TerraFX.Interop.Windows.ERROR;
@ -245,6 +244,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return default(LRESULT);
}
if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.AnyWindow))
ImGui.ClearWindowFocus();
break;
}
@ -371,6 +373,14 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_DISPLAYCHANGE:
this.viewportHandler.UpdateMonitors();
break;
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
ReleaseCapture();
ImGui.GetIO().WantCaptureMouse = false;
ImGui.ClearWindowFocus();
break;
}
return null;
@ -531,7 +541,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// We still want to return MA_NOACTIVATE
// https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate
return 0x3;
return MA.MA_NOACTIVATE;
case WM.WM_NCHITTEST:
// Let mouse pass-through the window. This will allow the backend to set io.MouseHoveredViewport properly (which is OPTIONAL).
// The ImGuiViewportFlags_NoInputs flag is set while dragging a viewport, as want to detect the window behind the one we are dragging.
@ -539,8 +549,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// your main loop after calling UpdatePlatformWindows(). Iterate all viewports/platform windows and pass the flag to your windowing system.
if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoInputs))
{
// https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest
return -1;
return HTTRANSPARENT;
}
break;
@ -575,51 +584,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private struct ViewportHandler : IDisposable
{
[SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Keeping references alive")]
private readonly List<object> delegateReferences = new();
private static readonly string WindowClassName = typeof(ViewportHandler).FullName!;
private Win32InputHandler input;
private nint classNamePtr;
private bool wantUpdateMonitors = true;
public ViewportHandler(Win32InputHandler input)
{
this.input = input;
this.classNamePtr = Marshal.StringToHGlobalUni("ImGui Platform");
var pio = ImGui.GetPlatformIO();
pio.PlatformCreateWindow = this.RegisterFunctionPointer<CreateWindowDelegate>(this.OnCreateWindow);
pio.PlatformDestroyWindow = this.RegisterFunctionPointer<DestroyWindowDelegate>(this.OnDestroyWindow);
pio.PlatformShowWindow = this.RegisterFunctionPointer<ShowWindowDelegate>(this.OnShowWindow);
pio.PlatformSetWindowPos = this.RegisterFunctionPointer<SetWindowPosDelegate>(this.OnSetWindowPos);
pio.PlatformGetWindowPos = this.RegisterFunctionPointer<GetWindowPosDelegate>(this.OnGetWindowPos);
pio.PlatformSetWindowSize = this.RegisterFunctionPointer<SetWindowSizeDelegate>(this.OnSetWindowSize);
pio.PlatformGetWindowSize = this.RegisterFunctionPointer<GetWindowSizeDelegate>(this.OnGetWindowSize);
pio.PlatformSetWindowFocus = this.RegisterFunctionPointer<SetWindowFocusDelegate>(this.OnSetWindowFocus);
pio.PlatformGetWindowFocus = this.RegisterFunctionPointer<GetWindowFocusDelegate>(this.OnGetWindowFocus);
pio.PlatformGetWindowMinimized =
this.RegisterFunctionPointer<GetWindowMinimizedDelegate>(this.OnGetWindowMinimized);
pio.PlatformSetWindowTitle = this.RegisterFunctionPointer<SetWindowTitleDelegate>(this.OnSetWindowTitle);
pio.PlatformSetWindowAlpha = this.RegisterFunctionPointer<SetWindowAlphaDelegate>(this.OnSetWindowAlpha);
pio.PlatformUpdateWindow = this.RegisterFunctionPointer<UpdateWindowDelegate>(this.OnUpdateWindow);
pio.PlatformCreateWindow = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, void>)&OnCreateWindow;
pio.PlatformDestroyWindow = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, void>)&OnDestroyWindow;
pio.PlatformShowWindow = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, void>)&OnShowWindow;
pio.PlatformSetWindowPos = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, Vector2, void>)&OnSetWindowPos;
pio.PlatformGetWindowPos = (delegate* unmanaged[Cdecl]<Vector2*, ImGuiViewportPtr, Vector2*>)&OnGetWindowPos;
pio.PlatformSetWindowSize = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, Vector2, void>)&OnSetWindowSize;
pio.PlatformGetWindowSize = (delegate* unmanaged[Cdecl]<Vector2*, ImGuiViewportPtr, Vector2*>)&OnGetWindowSize;
pio.PlatformSetWindowFocus = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, void>)&OnSetWindowFocus;
pio.PlatformGetWindowFocus = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, byte>)&OnGetWindowFocus;
pio.PlatformGetWindowMinimized = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, byte>)&OnGetWindowMinimized;
pio.PlatformSetWindowTitle = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, byte*, void>)&OnSetWindowTitle;
pio.PlatformSetWindowAlpha = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, float, void>)&OnSetWindowAlpha;
pio.PlatformUpdateWindow = (delegate* unmanaged[Cdecl]<ImGuiViewportPtr, void>)&OnUpdateWindow;
// pio.Platform_SetImeInputPos = this.RegisterFunctionPointer<SetImeInputPosDelegate>(this.OnSetImeInputPos);
// pio.Platform_GetWindowDpiScale = this.RegisterFunctionPointer<GetWindowDpiScaleDelegate>(this.OnGetWindowDpiScale);
// pio.Platform_ChangedViewport = this.RegisterFunctionPointer<ChangedViewportDelegate>(this.OnChangedViewport);
var wcex = new WNDCLASSEXW
fixed (char* windowClassNamePtr = WindowClassName)
{
cbSize = (uint)sizeof(WNDCLASSEXW),
style = CS.CS_HREDRAW | CS.CS_VREDRAW,
hInstance = GetModuleHandleW(null),
hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND),
lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal
.GetFunctionPointerForDelegate(this.input.wndProcDelegate),
lpszClassName = (ushort*)this.classNamePtr,
};
var wcex = new WNDCLASSEXW
{
cbSize = (uint)sizeof(WNDCLASSEXW),
style = CS.CS_HREDRAW | CS.CS_VREDRAW,
hInstance = (HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module),
hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND),
lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal
.GetFunctionPointerForDelegate(this.input.wndProcDelegate),
lpszClassName = (ushort*)windowClassNamePtr,
};
if (RegisterClassExW(&wcex) == 0)
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new("RegisterClassEx Fail");
if (RegisterClassExW(&wcex) == 0)
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new("RegisterClassEx Fail");
}
// Register main window handle (which is owned by the main application, not by us)
// This is mostly for simplicity and consistency, so that our code (e.g. mouse handling etc.) can use same logic for main and secondary viewports.
@ -647,11 +655,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
ImGui.GetPlatformIO().Handle->Monitors = default;
}
if (this.classNamePtr != 0)
fixed (char* windowClassNamePtr = WindowClassName)
{
UnregisterClassW((ushort*)this.classNamePtr, GetModuleHandleW(null));
Marshal.FreeHGlobal(this.classNamePtr);
this.classNamePtr = 0;
UnregisterClassW(
(ushort*)windowClassNamePtr,
(HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module));
}
pio.PlatformCreateWindow = null;
@ -740,13 +748,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
}
}
private void* RegisterFunctionPointer<T>(T obj)
{
this.delegateReferences.Add(obj);
return Marshal.GetFunctionPointerForDelegate(obj).ToPointer();
}
private void OnCreateWindow(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnCreateWindow(ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiViewportDataWin32>());
viewport.PlatformUserData = data;
@ -774,12 +777,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
};
AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle);
fixed (char* pwszWindowTitle = "Untitled")
fixed (char* windowClassNamePtr = WindowClassName)
{
data->Hwnd = CreateWindowExW(
(uint)data->DwExStyle,
(ushort*)this.classNamePtr,
(ushort*)pwszWindowTitle,
(ushort*)windowClassNamePtr,
(ushort*)windowClassNamePtr,
(uint)data->DwStyle,
rect.left,
rect.top,
@ -787,8 +790,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
rect.bottom - rect.top,
parentWindow,
default,
GetModuleHandleW(null),
default);
(HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module),
null);
}
data->HwndOwned = true;
@ -796,7 +799,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;
}
private void OnDestroyWindow(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnDestroyWindow(ImGuiViewportPtr viewport)
{
// This is also called on the main viewport for some reason, and we never set that viewport's PlatformUserData
if (viewport.PlatformUserData == null) return;
@ -807,7 +811,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{
// Transfer capture so if we started dragging from a window that later disappears, we'll still receive the MOUSEUP event.
ReleaseCapture();
SetCapture(this.input.hWnd);
if (viewport.ParentViewportId != 0)
{
var parentViewport = ImGui.FindViewportByID(viewport.ParentViewportId);
SetCapture((HWND)parentViewport.PlatformHandle);
}
}
if (data->Hwnd != nint.Zero && data->HwndOwned)
@ -826,7 +834,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
viewport.PlatformUserData = viewport.PlatformHandle = null;
}
private void OnShowWindow(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnShowWindow(ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
@ -836,7 +845,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
ShowWindow(data->Hwnd, SW.SW_SHOW);
}
private void OnUpdateWindow(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnUpdateWindow(ImGuiViewportPtr viewport)
{
// (Optional) Update Win32 style if it changed _after_ creation.
// Generally they won't change unless configuration flags are changed, but advanced uses (such as manually rewriting viewport flags) make this useful.
@ -897,17 +907,18 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
}
}
private Vector2* OnGetWindowPos(Vector2* returnStorage, ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static Vector2* OnGetWindowPos(Vector2* returnValueStorage, ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
var pt = new POINT { x = 0, y = 0 };
ClientToScreen(data->Hwnd, &pt);
returnStorage->X = pt.x;
returnStorage->Y = pt.y;
return returnStorage;
*returnValueStorage = new(pt.x, pt.y);
return returnValueStorage;
}
private void OnSetWindowPos(ImGuiViewportPtr viewport, Vector2 pos)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnSetWindowPos(ImGuiViewportPtr viewport, Vector2 pos)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
var rect = new RECT((int)pos.X, (int)pos.Y, (int)pos.X, (int)pos.Y);
@ -924,17 +935,18 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
SWP.SWP_NOACTIVATE);
}
private Vector2* OnGetWindowSize(Vector2* returnStorage, ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static Vector2* OnGetWindowSize(Vector2* returnValueStorage, ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
RECT rect;
GetClientRect(data->Hwnd, &rect);
returnStorage->X = rect.right - rect.left;
returnStorage->Y = rect.bottom - rect.top;
return returnStorage;
*returnValueStorage = new(rect.right - rect.left, rect.bottom - rect.top);
return returnValueStorage;
}
private void OnSetWindowSize(ImGuiViewportPtr viewport, Vector2 size)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnSetWindowSize(ImGuiViewportPtr viewport, Vector2 size)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
@ -952,7 +964,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
SWP.SWP_NOACTIVATE);
}
private void OnSetWindowFocus(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnSetWindowFocus(ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
@ -961,26 +974,30 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
SetFocus(data->Hwnd);
}
private bool OnGetWindowFocus(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static byte OnGetWindowFocus(ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
return GetForegroundWindow() == data->Hwnd;
return GetForegroundWindow() == data->Hwnd ? (byte)1 : (byte)0;
}
private bool OnGetWindowMinimized(ImGuiViewportPtr viewport)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static byte OnGetWindowMinimized(ImGuiViewportPtr viewport)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
return IsIconic(data->Hwnd);
return IsIconic(data->Hwnd) ? (byte)1 : (byte)0;
}
private void OnSetWindowTitle(ImGuiViewportPtr viewport, string title)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnSetWindowTitle(ImGuiViewportPtr viewport, byte* title)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
fixed (char* pwszTitle = title)
fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title))
SetWindowTextW(data->Hwnd, (ushort*)pwszTitle);
}
private void OnSetWindowAlpha(ImGuiViewportPtr viewport, float alpha)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnSetWindowAlpha(ImGuiViewportPtr viewport, float alpha)
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
var style = GetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE);

View file

@ -223,15 +223,15 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
ImDrawDataPtr drawData,
bool clearRenderTarget)
{
// Do nothing when there's nothing to draw
if (drawData.IsNull || !drawData.Valid)
return;
// Avoid rendering when minimized
if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0)
return;
using var oldState = new D3D11DeviceContextStateBackup(this.featureLevel, this.context.Get());
// Setup desired DX state
this.SetupRenderState(drawData);
// Set up render target
this.context.Get()->OMSetRenderTargets(1, &renderTargetView, null);
if (clearRenderTarget)
{
@ -239,17 +239,17 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
this.context.Get()->ClearRenderTargetView(renderTargetView, (float*)&color);
}
if (!drawData.Valid || drawData.CmdListsCount == 0)
return;
// Stop if there's nothing to draw
var cmdLists = new Span<ImDrawListPtr>(drawData.Handle->CmdLists, drawData.Handle->CmdListsCount);
if (cmdLists.IsEmpty)
return;
// Create and grow vertex/index buffers if needed
if (this.vertexBufferSize < drawData.TotalVtxCount)
this.vertexBuffer.Dispose();
if (this.vertexBuffer.Get() is null)
{
this.vertexBufferSize = drawData.TotalVtxCount + 5000;
this.vertexBufferSize = drawData.TotalVtxCount + 8192;
var desc = new D3D11_BUFFER_DESC(
(uint)(sizeof(ImDrawVert) * this.vertexBufferSize),
(uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
@ -264,7 +264,7 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
this.indexBuffer.Dispose();
if (this.indexBuffer.Get() is null)
{
this.indexBufferSize = drawData.TotalIdxCount + 5000;
this.indexBufferSize = drawData.TotalIdxCount + 16384;
var desc = new D3D11_BUFFER_DESC(
(uint)(sizeof(ushort) * this.indexBufferSize),
(uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER,
@ -275,9 +275,14 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
this.indexBuffer.Attach(buffer);
}
// Upload vertex/index data into a single contiguous GPU buffer
using var oldState = new D3D11DeviceContextStateBackup(this.featureLevel, this.context.Get());
// Setup desired DX state
this.SetupRenderState(drawData);
try
{
// Upload vertex/index data into a single contiguous GPU buffer.
var vertexData = default(D3D11_MAPPED_SUBRESOURCE);
var indexData = default(D3D11_MAPPED_SUBRESOURCE);
this.context.Get()->Map(
@ -306,26 +311,18 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
targetVertices = targetVertices[vertices.Length..];
targetIndices = targetIndices[indices.Length..];
}
}
finally
{
this.context.Get()->Unmap((ID3D11Resource*)this.vertexBuffer.Get(), 0);
this.context.Get()->Unmap((ID3D11Resource*)this.indexBuffer.Get(), 0);
}
// Setup orthographic projection matrix into our constant buffer.
// Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB).
// DisplayPos is (0,0) for single viewport apps.
try
{
var data = default(D3D11_MAPPED_SUBRESOURCE);
// Setup orthographic projection matrix into our constant buffer.
// Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB).
// DisplayPos is (0,0) for single viewport apps.
var constantBufferData = default(D3D11_MAPPED_SUBRESOURCE);
this.context.Get()->Map(
(ID3D11Resource*)this.vertexConstantBuffer.Get(),
0,
D3D11_MAP.D3D11_MAP_WRITE_DISCARD,
0,
&data).ThrowOnError();
*(Matrix4x4*)data.pData = Matrix4x4.CreateOrthographicOffCenter(
&constantBufferData).ThrowOnError();
*(Matrix4x4*)constantBufferData.pData = Matrix4x4.CreateOrthographicOffCenter(
drawData.DisplayPos.X,
drawData.DisplayPos.X + drawData.DisplaySize.X,
drawData.DisplayPos.Y + drawData.DisplaySize.Y,
@ -335,6 +332,8 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
}
finally
{
this.context.Get()->Unmap((ID3D11Resource*)this.vertexBuffer.Get(), 0);
this.context.Get()->Unmap((ID3D11Resource*)this.indexBuffer.Get(), 0);
this.context.Get()->Unmap((ID3D11Resource*)this.vertexConstantBuffer.Get(), 0);
}
@ -343,8 +342,6 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
var vertexOffset = 0;
var indexOffset = 0;
var clipOff = new Vector4(drawData.DisplayPos, drawData.DisplayPos.X, drawData.DisplayPos.Y);
this.context.Get()->PSSetShader(this.pixelShader, null, 0);
this.context.Get()->PSSetSamplers(0, 1, this.sampler.GetAddressOf());
foreach (ref var cmdList in cmdLists)
{
var cmds = new ImVectorWrapper<ImDrawCmd>(cmdList.Handle->CmdBuffer.ToUntyped());
@ -383,8 +380,8 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
default:
{
// User callback, registered via ImDrawList::AddCallback()
var cb = (delegate*<ImDrawListPtr, ref ImDrawCmd, void>)cmd.UserCallback;
cb(cmdList, ref cmd);
var cb = (delegate* unmanaged<ImDrawListPtr, ImDrawCmdPtr, void>)cmd.UserCallback;
cb(cmdList, (ImDrawCmdPtr)Unsafe.AsPointer(ref cmd));
break;
}
}
@ -467,7 +464,8 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer
buffer = this.vertexConstantBuffer.Get();
ctx->VSSetConstantBuffers(0, 1, &buffer);
// PS handled later
ctx->PSSetShader(this.pixelShader, null, 0);
ctx->PSSetSamplers(0, 1, this.sampler.GetAddressOf());
ctx->GSSetShader(null, null, 0);
ctx->HSSetShader(null, null, 0);

View file

@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
using Serilog;
@ -55,7 +57,8 @@ internal class AssertHandler : IDisposable
/// </summary>
public unsafe void Setup()
{
CustomNativeFunctions.igCustom_SetAssertCallback(Marshal.GetFunctionPointerForDelegate(this.callback).ToPointer());
CustomNativeFunctions.igCustom_SetAssertCallback(
Marshal.GetFunctionPointerForDelegate(this.callback).ToPointer());
}
/// <summary>
@ -72,16 +75,52 @@ internal class AssertHandler : IDisposable
this.Shutdown();
}
private static string? ExtractImguiFunction(StackTrace stackTrace)
{
var frame = stackTrace.GetFrames()
.FirstOrDefault(f => f.GetMethod()?.DeclaringType?.Namespace == "Dalamud.Bindings.ImGui");
if (frame == null)
return null;
var method = frame.GetMethod();
if (method == null)
return null;
return $"{method.Name}({string.Join(", ", method.GetParameters().Select(p => p.Name))})";
}
private static StackTrace GenerateStackTrace()
{
var trace = DiagnosticUtil.GetUsefulTrace(new StackTrace(true));
var frames = trace.GetFrames().ToList();
// Remove everything that happens in the assert context.
var lastAssertIdx = frames.FindLastIndex(f => f.GetMethod()?.DeclaringType == typeof(AssertHandler));
if (lastAssertIdx >= 0)
{
frames.RemoveRange(0, lastAssertIdx + 1);
}
var firstInterfaceManagerIdx = frames.FindIndex(f => f.GetMethod()?.DeclaringType == typeof(InterfaceManager));
if (firstInterfaceManagerIdx >= 0)
{
frames.RemoveRange(firstInterfaceManagerIdx, frames.Count - firstInterfaceManagerIdx);
}
return new StackTrace(frames);
}
private unsafe void OnImGuiAssert(void* pExpr, void* pFile, int line)
{
var expr = Marshal.PtrToStringAnsi(new IntPtr(pExpr));
var file = Marshal.PtrToStringAnsi(new IntPtr(pFile));
if (expr == null || file == null)
{
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (failed to parse)",
expr,
file,
line);
Log.Warning(
"ImGui assertion failed: {Expr} at {File}:{Line} (failed to parse)",
expr,
file,
line);
return;
}
@ -93,7 +132,7 @@ internal class AssertHandler : IDisposable
if (!this.ShowAsserts && !this.everShownAssertThisSession)
return;
Lazy<string> stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace()).ToString());
Lazy<StackTrace> stackTrace = new(GenerateStackTrace);
if (!this.EnableVerboseLogging)
{
@ -103,11 +142,12 @@ internal class AssertHandler : IDisposable
if (count <= HideThreshold || count % HidePrintEvery == 0)
{
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
expr,
file,
line,
count);
Log.Warning(
"ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
expr,
file,
line,
count);
}
}
else
@ -117,11 +157,12 @@ internal class AssertHandler : IDisposable
}
else
{
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
expr,
file,
line,
stackTrace.Value);
Log.Warning(
"ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
expr,
file,
line,
stackTrace.Value.ToString());
}
if (!this.ShowAsserts)
@ -145,7 +186,8 @@ internal class AssertHandler : IDisposable
}
// grab the stack trace now that we've decided to show UI.
_ = stackTrace.Value;
var responsiblePlugin = Service<PluginManager>.GetNullable()?.FindCallingPlugin(stackTrace.Value);
var responsibleMethodCall = ExtractImguiFunction(stackTrace.Value);
var gitHubUrl = GetRepoUrl();
var showOnGitHubButton = new TaskDialogButton
@ -175,12 +217,37 @@ internal class AssertHandler : IDisposable
var ignoreButton = TaskDialogButton.Ignore;
TaskDialogButton? result = null;
void DialogThreadStart()
{
// TODO(goat): This is probably not gonna work if we showed the loading dialog
// this session since it already loaded visual styles...
Application.EnableVisualStyles();
string text;
if (responsiblePlugin != null)
{
text = $"The plugin \"{responsiblePlugin.Name}\" appears to have caused an ImGui assertion failure. " +
$"Please report this problem to the plugin's developer.\n\n";
}
else
{
text = "Some code in a plugin or Dalamud itself has caused an ImGui assertion failure. " +
"Please report this problem in the Dalamud discord.\n\n";
}
text += $"You may attempt to continue running the game, but Dalamud UI elements may not work " +
$"correctly, or the game may crash after resuming.\n\n";
if (responsibleMethodCall != null)
{
text += $"Assertion failed: {expr} when performing {responsibleMethodCall}\n{file}:{line}";
}
else
{
text += $"Assertion failed: {expr}\nAt: {file}:{line}";
}
var page = new TaskDialogPage
{
Heading = "ImGui assertion failed",
@ -189,9 +256,9 @@ internal class AssertHandler : IDisposable
{
CollapsedButtonText = "Show stack trace",
ExpandedButtonText = "Hide stack trace",
Text = stackTrace.Value,
Text = stackTrace.Value.ToString(),
},
Text = $"Some code in a plugin or Dalamud itself has caused an internal assertion in ImGui to fail. The game will most likely crash now.\n\n{expr}\nAt: {file}:{line}",
Text = text,
Icon = TaskDialogIcon.Warning,
Buttons =
[

View file

@ -2,11 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
@ -46,24 +44,24 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
.ToDictionary(x => x.Item1, x => x.Name);
private static readonly UnicodeRange[] HanRange =
{
[
UnicodeRanges.CjkRadicalsSupplement,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.CjkUnifiedIdeographsExtensionA,
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkCompatibilityIdeographs,
UnicodeRanges.CjkCompatibilityForms,
UnicodeRanges.CjkCompatibilityForms
// No more; Extension B~ are outside BMP range
};
];
private static readonly UnicodeRange[] HangulRange =
{
[
UnicodeRanges.HangulJamo,
UnicodeRanges.HangulSyllables,
UnicodeRanges.HangulCompatibilityJamo,
UnicodeRanges.HangulJamoExtendedA,
UnicodeRanges.HangulJamoExtendedB,
};
UnicodeRanges.HangulJamoExtendedB
];
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
@ -109,24 +107,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
private bool updateInputLanguage = true;
private bool updateImeStatusAgain;
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")]
static DalamudIme()
{
nint cimgui;
try
{
_ = ImGui.GetCurrentContext();
cimgui = Process.GetCurrentProcess().Modules.Cast<ProcessModule>()
.First(x => x.ModuleName == "cimgui.dll")
.BaseAddress;
}
catch
{
return;
}
}
[ServiceManager.ServiceConstructor]
private DalamudIme(InterfaceManager.InterfaceManagerWithScene imws)
{
@ -170,11 +150,11 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
if (!ImGui.GetIO().ConfigInputTextCursorBlink)
return true;
var textState = GetInputTextState();
if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0)
if (textState.ID == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0)
return true;
if (textState->CursorAnim <= 0)
if (textState.CursorAnim <= 0)
return true;
return textState->CursorAnim % 1.2f <= 0.8f;
return textState.CursorAnim % 1.2f <= 0.8f;
}
}
@ -227,11 +207,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
}
}
private static ImGuiInputTextStateWrapper* GetInputTextState()
{
var ctx = ImGui.GetCurrentContext();
return (ImGuiInputTextStateWrapper*)&ctx.Handle->InputTextState;
}
private static ImGuiInputTextStatePtr GetInputTextState() => new(&ImGui.GetCurrentContext().Handle->InputTextState);
private static (string String, bool Supported) ToUcs2(char* data, int nc = -1)
{
@ -332,7 +308,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
try
{
var textState = GetInputTextState();
var invalidTarget = textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
var invalidTarget = textState.ID == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0;
#if IMEDEBUG
switch (args.Message)
@ -564,17 +540,17 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
var textState = GetInputTextState();
if (this.temporaryUndoSelection is not null)
{
textState->Undo();
textState->SelectionTuple = this.temporaryUndoSelection.Value;
textState.Undo();
textState.SetSelectionTuple(this.temporaryUndoSelection.Value);
this.temporaryUndoSelection = null;
}
textState->SanitizeSelectionRange();
if (textState->ReplaceSelectionAndPushUndo(newString))
this.temporaryUndoSelection = textState->SelectionTuple;
textState.SanitizeSelectionRange();
if (textState.ReplaceSelectionAndPushUndo(newString))
this.temporaryUndoSelection = textState.GetSelectionTuple();
// Put the cursor at the beginning, so that the candidate window appears aligned with the text.
textState->SetSelectionRange(textState->SelectionTuple.Start, newString.Length, 0);
textState.SetSelectionRange(textState.GetSelectionTuple().Start, newString.Length, 0);
if (finalCommit)
{
@ -621,7 +597,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
this.temporaryUndoSelection = null;
var textState = GetInputTextState();
textState->Stb.SelectStart = textState->Stb.Cursor = textState->Stb.SelectEnd;
textState.Stb.SelectStart = textState.Stb.Cursor = textState.Stb.SelectEnd;
this.candidateStrings.Clear();
this.immCandNative = default;
@ -931,185 +907,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
}
}
/// <summary>
/// Ported from imstb_textedit.h.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0xE2C)]
private struct StbTextEditState
{
/// <summary>
/// Position of the text cursor within the string.
/// </summary>
public int Cursor;
/// <summary>
/// Selection start point.
/// </summary>
public int SelectStart;
/// <summary>
/// selection start and end point in characters; if equal, no selection.
/// </summary>
/// <remarks>
/// Note that start may be less than or greater than end (e.g. when dragging the mouse,
/// start is where the initial click was, and you can drag in either direction.)
/// </remarks>
public int SelectEnd;
/// <summary>
/// Each text field keeps its own insert mode state.
/// To keep an app-wide insert mode, copy this value in/out of the app state.
/// </summary>
public byte InsertMode;
/// <summary>
/// Page size in number of row.
/// This value MUST be set to >0 for pageup or pagedown in multilines documents.
/// </summary>
public int RowCountPerPage;
// Remainder is stb-private data.
}
[StructLayout(LayoutKind.Sequential)]
private struct ImGuiInputTextStateWrapper
{
public uint Id;
public int CurLenW;
public int CurLenA;
public ImVector<char> TextWRaw;
public ImVector<byte> TextARaw;
public ImVector<byte> InitialTextARaw;
public bool TextAIsValid;
public int BufCapacityA;
public float ScrollX;
public StbTextEditState Stb;
public float CursorAnim;
public bool CursorFollow;
public bool SelectedAllMouseLock;
public bool Edited;
public ImGuiInputTextFlags Flags;
public ImVectorWrapper<char> TextW => new((ImVector*)&this.ThisWrapperPtr->TextWRaw);
public (int Start, int End, int Cursor) SelectionTuple
{
get => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor);
set => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor) = value;
}
private ImGuiInputTextStateWrapper* ThisWrapperPtr => (ImGuiInputTextStateWrapper*)Unsafe.AsPointer(ref this);
private ImGuiInputTextState* ThisPtr => (ImGuiInputTextState*)Unsafe.AsPointer(ref this);
public void SetSelectionRange(int offset, int length, int relativeCursorOffset)
{
this.Stb.SelectStart = offset;
this.Stb.SelectEnd = offset + length;
if (relativeCursorOffset >= 0)
this.Stb.Cursor = this.Stb.SelectStart + relativeCursorOffset;
else
this.Stb.Cursor = this.Stb.SelectEnd + 1 + relativeCursorOffset;
this.SanitizeSelectionRange();
}
public void SanitizeSelectionRange()
{
ref var s = ref this.Stb.SelectStart;
ref var e = ref this.Stb.SelectEnd;
ref var c = ref this.Stb.Cursor;
s = Math.Clamp(s, 0, this.CurLenW);
e = Math.Clamp(e, 0, this.CurLenW);
c = Math.Clamp(c, 0, this.CurLenW);
if (s == e)
s = e = c;
if (s > e)
(s, e) = (e, s);
}
public void Undo() => ImGuiP.Custom_StbTextUndo(this.ThisPtr);
public bool MakeUndoReplace(int offset, int oldLength, int newLength)
{
if (oldLength == 0 && newLength == 0)
return false;
ImGuiP.Custom_StbTextMakeUndoReplace(this.ThisPtr, offset, oldLength, newLength);
return true;
}
public bool ReplaceSelectionAndPushUndo(ReadOnlySpan<char> newText)
{
var off = this.Stb.SelectStart;
var len = this.Stb.SelectEnd - this.Stb.SelectStart;
return this.MakeUndoReplace(off, len, newText.Length) && this.ReplaceChars(off, len, newText);
}
public bool ReplaceChars(int pos, int len, ReadOnlySpan<char> newText)
{
this.DeleteChars(pos, len);
return this.InsertChars(pos, newText);
}
// See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS
public void DeleteChars(int pos, int n)
{
if (n == 0)
return;
var dst = this.TextW.Data + pos;
// We maintain our buffer length in both UTF-8 and wchar formats
this.Edited = true;
this.CurLenA -= Encoding.UTF8.GetByteCount(dst, n);
this.CurLenW -= n;
// Offset remaining text (FIXME-OPT: Use memmove)
var src = this.TextW.Data + pos + n;
int i;
for (i = 0; src[i] != 0; i++)
dst[i] = src[i];
dst[i] = '\0';
}
// See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS
public bool InsertChars(int pos, ReadOnlySpan<char> newText)
{
if (newText.Length == 0)
return true;
var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0;
var textLen = this.CurLenW;
Debug.Assert(pos <= textLen, "pos <= text_len");
var newTextLenUtf8 = Encoding.UTF8.GetByteCount(newText);
if (!isResizable && newTextLenUtf8 + this.CurLenA + 1 > this.BufCapacityA)
return false;
// Grow internal buffer if needed
if (newText.Length + textLen + 1 > this.TextW.Length)
{
if (!isResizable)
return false;
Debug.Assert(textLen < this.TextW.Length, "text_len < this.TextW.Length");
this.TextW.Resize(textLen + Math.Clamp(newText.Length * 4, 32, Math.Max(256, newText.Length)) + 1);
}
var text = this.TextW.DataSpan;
if (pos != textLen)
text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]);
newText.CopyTo(text[pos..]);
this.Edited = true;
this.CurLenW += newText.Length;
this.CurLenA += newTextLenUtf8;
this.TextW[this.CurLenW] = '\0';
return true;
}
}
#if IMEDEBUG
private static class ImeDebug
{

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