Skip to content

Commit 41bb687

Browse files
committed
Add FOCUSTARGET / TARGETFOCUS bindings under TARGETING
Splice into the engine's FrameXML Bindings.xml at load time via a file-read hook so the new bindings receive display indices from the same running counter as native bindings — landing visually inside the TARGETING block instead of orphaned at the bottom (where addon-side Bindings.xml entries always render, regardless of header="TARGETING").
1 parent d9cc3f9 commit 41bb687

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
BINDING_NAME_FOCUSTARGET = "Focus Target";
2+
BINDING_NAME_TARGETFOCUS = "Target Focus";
3+
FOCUSTARGET = "Focus Target";
14
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
25
ITEMS_EQUIPPED = "%d |4item:items; equipped";
36
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
47
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
58
ITEM_MISSING = "%s missing";
69
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
10+
TARGETFOCUS = "Target Focus";

src/addons/Embedded.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
#include "Game.h"
4949
#include "Offsets.h"
50+
#include "bindings/Inject.h"
5051

5152
#include <cstdint>
5253
#include <cstdio>
@@ -248,6 +249,14 @@ void DecideSource() {
248249
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
249250
size_t *outSize, size_t extraBytes,
250251
int flag1, int flag2) {
252+
// FrameXML Bindings.xml gets an inline splice for our TARGETING
253+
// additions — see `bindings/Inject.cpp`. Returns true with the
254+
// patched buffer ready for the engine's binding parser to consume.
255+
if (Bindings::Inject::TryHandle(unused, path, outBuf, outSize,
256+
extraBytes, flag1, flag2, FileRead_o)) {
257+
return 1;
258+
}
259+
251260
const char *suffix = StripAddonPrefix(path);
252261
if (suffix == nullptr) {
253262
// Path isn't under `Interface\AddOns\!!!ClassicAPI\` — straight

src/bindings/Inject.cpp

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU Lesser General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU Lesser General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// Inject ClassicAPI-owned bindings into the engine's TARGETING group.
15+
//
16+
// Why a file-read hook rather than an addon-side Bindings.xml: vanilla
17+
// stores every binding in a single linearly-indexed table, and the
18+
// keybind UI walks that table in index order with `HEADER_*` pseudo-
19+
// entries splitting groups. Addon Bindings.xml files get parsed
20+
// AFTER FrameXML, so even with `header="TARGETING"` an addon binding
21+
// receives a display index higher than every engine binding — it
22+
// renders orphaned at the very bottom of the keybind list rather than
23+
// under TARGETING.
24+
//
25+
// To land inside TARGETING, the binding's display index has to fall
26+
// between HEADER_TARGETING and HEADER_INTERFACE. The cleanest way to
27+
// arrange that is to splice the bindings into the FrameXML file the
28+
// engine itself parses, so they take their indices from the same
29+
// running counter every native binding does.
30+
//
31+
// Splice site: just before `<Binding name="TOGGLECHARACTER0"` (the
32+
// first INTERFACE-section entry). Our injected bindings inherit
33+
// TARGETING from the preceding `header="TARGETING"` declaration on
34+
// `TARGETNEARESTENEMY` (the engine never re-asserts a header on each
35+
// binding — group membership is positional). They appear at the tail
36+
// of TARGETING, just after PETATTACK / ATTACKTARGET.
37+
//
38+
// The engine's binding parser is `FUN_004b6f70`. It calls
39+
// `FUN_FILE_READ` (`0x00648620`) to load the file, then walks `<Binding>`
40+
// elements assigning each the next display index from a running
41+
// counter. By the time our injected bindings are processed, the
42+
// engine has already emitted HEADER_TARGETING and a sequence of
43+
// TARGETING entries; ours append to that sequence and HEADER_INTERFACE
44+
// follows.
45+
46+
#include "Inject.h"
47+
#include "Offsets.h"
48+
49+
#include <cstring>
50+
51+
namespace Bindings::Inject {
52+
53+
namespace {
54+
55+
constexpr const char *kFrameXMLBindingsPath = "Interface\\FrameXML\\Bindings.xml";
56+
57+
// First INTERFACE-section binding. Splicing immediately before this
58+
// puts our entries at the tail of the TARGETING block.
59+
constexpr const char *kSpliceMarker = "<Binding name=\"TOGGLECHARACTER0\"";
60+
61+
// LF-only line endings — vanilla's XML parser is whitespace-tolerant
62+
// and accepts either LF or CRLF freely. Indentation matches the
63+
// surrounding file (one leading tab per `<Binding>`).
64+
constexpr const char *kInjectedXml =
65+
"<Binding name=\"FOCUSTARGET\">\n"
66+
"\t\tFocusUnit(\"target\");\n"
67+
"\t</Binding>\n"
68+
"\t<Binding name=\"TARGETFOCUS\">\n"
69+
"\t\tTargetUnit(\"focus\");\n"
70+
"\t</Binding>\n"
71+
"\t";
72+
73+
char NormalizeChar(char c) {
74+
if (c == '/') return '\\';
75+
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
76+
return c;
77+
}
78+
79+
bool PathEqualsCI(const char *a, const char *b) {
80+
while (*a != '\0' && *b != '\0') {
81+
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
82+
++a; ++b;
83+
}
84+
return *a == *b;
85+
}
86+
87+
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
88+
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
89+
90+
const char *FindMarker(const char *content, size_t size, const char *marker) {
91+
const size_t markerLen = std::strlen(marker);
92+
if (size < markerLen) return nullptr;
93+
for (size_t i = 0; i + markerLen <= size; ++i) {
94+
if (std::memcmp(content + i, marker, markerLen) == 0)
95+
return content + i;
96+
}
97+
return nullptr;
98+
}
99+
100+
} // namespace
101+
102+
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
103+
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
104+
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
105+
return false;
106+
107+
// Pull the real file through the original reader with the caller's
108+
// requested `extraBytes` honored — the original allocates a Storm
109+
// buffer of `fileSize + extraBytes` and zero-fills the tail.
110+
void *origBuf = nullptr;
111+
size_t origSize = 0;
112+
const int readOk = original(unused, path, &origBuf, &origSize,
113+
extraBytes, flag1, flag2);
114+
if (readOk == 0 || origBuf == nullptr)
115+
return false;
116+
117+
const char *content = static_cast<const char *>(origBuf);
118+
const char *splice = FindMarker(content, origSize, kSpliceMarker);
119+
if (splice == nullptr) {
120+
// Marker missing — likely a modified or non-vanilla FrameXML
121+
// (Turtle patch level, custom build). Pass the original
122+
// through unchanged rather than corrupting it.
123+
if (outBuf != nullptr) *outBuf = origBuf;
124+
if (outSize != nullptr) *outSize = origSize;
125+
return true;
126+
}
127+
128+
const size_t injectLen = std::strlen(kInjectedXml);
129+
const size_t splicePos = static_cast<size_t>(splice - content);
130+
const size_t newSize = origSize + injectLen;
131+
132+
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
133+
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
134+
135+
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
136+
if (newBuf == nullptr) {
137+
// Allocation failed — fall back to the original buffer so the
138+
// engine still loads its own bindings.
139+
if (outBuf != nullptr) *outBuf = origBuf;
140+
if (outSize != nullptr) *outSize = origSize;
141+
return true;
142+
}
143+
144+
auto *dest = static_cast<char *>(newBuf);
145+
std::memcpy(dest, content, splicePos);
146+
std::memcpy(dest + splicePos, kInjectedXml, injectLen);
147+
std::memcpy(dest + splicePos + injectLen,
148+
content + splicePos, origSize - splicePos);
149+
if (extraBytes > 0)
150+
std::memset(dest + newSize, 0, extraBytes);
151+
152+
SMemFree(origBuf, __FILE__, __LINE__, 0);
153+
154+
if (outBuf != nullptr) *outBuf = newBuf;
155+
if (outSize != nullptr) *outSize = newSize;
156+
return true;
157+
}
158+
159+
} // namespace Bindings::Inject

src/bindings/Inject.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU Lesser General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU Lesser General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
#pragma once
15+
16+
#include <cstddef>
17+
18+
namespace Bindings::Inject {
19+
20+
// `FUN_FILE_READ` shape — Storm-style: __stdcall, returns 1 on success
21+
// with the buffer allocated via SMemAlloc into `*outBuf` (size in
22+
// `*outSize`, plus `extraBytes` of trailing zero padding).
23+
using FileReadFn = int(__stdcall *)(int unused, const char *path,
24+
void **outBuf, size_t *outSize,
25+
size_t extraBytes,
26+
int flag1, int flag2);
27+
28+
// Path-matches `Interface\FrameXML\Bindings.xml`. On match: fetches
29+
// the real file via `original`, splices our ClassicAPI bindings into
30+
// the TARGETING block, and returns `true` with `*outBuf` / `*outSize`
31+
// set to a fresh Storm buffer the caller can SMemFree normally.
32+
// Returns `false` for any other path — caller should fall through to
33+
// its own logic.
34+
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
35+
size_t extraBytes, int flag1, int flag2, FileReadFn original);
36+
37+
} // namespace Bindings::Inject

0 commit comments

Comments
 (0)