Skip to content

Commit 02ad0ea

Browse files
committed
feat(cvar): add C_CVar.GetCVarBitfield / SetCVarBitfield
Treat a cvar's numeric value as a bitfield and read or write one bit, so a single cvar carries many independent booleans. Upstream this backs the record of which tutorial popups a player has dismissed: one cvar, one bit per popup, persisted by the config machinery for free. Namespaced only. Blizzard's FrameXML calls the bare globals, but those are its own wrappers rather than the API, and nothing here needs them. Both wiki pages are stubs, so the details come from Blizzard's generated documentation: `index` is typed `luaIndex`, which is what settles that it is 1-based; the getter's return is nilable; and the setter names its requirements as a valid and public cvar, a non-read-only one, a non-secure one, and an index in range. The first two are questions this client can genuinely answer -- the lookup is the same filtering one behind C_CVar.DoesCVarExist, so a cvar preserved out of Config.wtf without being implemented is refused here exactly as by every other Lua cvar function -- and nothing here is ever secure. The value is parsed and rewritten as a uint64 in C++, never handed through a Lua number, so all 64 bits are exact and the 1..64 index range is the arithmetic type's own width rather than a limit chosen here. Writing goes through CVar::Factory::SetString rather than the engine setter directly: that wrapper already declares FUN_SET_CVAR_VALUE as the __thiscall it is, and getting that wrong corrupts the stack silently. It also means a bitfield write fires the change callback and marks the config dirty, exactly as SetCVar does. Verified in-game against a registered cvar: bits 1 and 5 give 17, clearing bit 1 gives 16 with bit 5 untouched, and bit 64 gives 9223372036854775824 and reads back true -- a value past Lua 5.0's exact integer range, which is what proves the arithmetic is not round-tripping through a double. Out-of-range indices read nil, and setting refuses for an out-of-range index, an unknown cvar, and a read-only one.
1 parent 1dee8e5 commit 02ad0ea

3 files changed

Lines changed: 173 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ reference in **[docs/API.md](docs/API.md)**.
6060
| [ColorUtil](docs/API.md#colorutil) | `C_ColorUtil.ConvertRGBToHSV`, `C_ColorUtil.ConvertHSVToRGB`, `C_ColorUtil.ConvertHSVToHSL`, `C_ColorUtil.ConvertHSLToHSV`, `C_ColorUtil.ConvertHSLToRGB`, `C_ColorUtil.GenerateTextColorCode`, `C_ColorUtil.WrapTextInColor`, `C_ColorUtil.WrapTextInColorCode` |
6161
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
6262
| [Console](docs/API.md#console) | `ConsoleGetAllCommands` |
63-
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo` |
63+
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.SetCVarBitfield` |
6464
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
6565
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
6666
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |

docs/API.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ build instructions.
9999
- [`C_CVar.GetCVarInfo(name)`](#c_cvargetcvarinfoname)
100100
- [`C_CVar.DoesCVarExist(name)`](#c_cvardoescvarexistname)
101101
- [`C_CVar.AreCVarsLoaded()`](#c_cvararecvarsloaded)
102+
- [`C_CVar.GetCVarBitfield(name, index)` / `C_CVar.SetCVarBitfield(name, index, value)`](#c_cvargetcvarbitfieldname-index--c_cvarsetcvarbitfieldname-index-value)
102103
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
103104

104105
- [Cursor](#cursor)
@@ -2533,6 +2534,33 @@ client starts, before there is any Lua to ask, and none of them are fetched
25332534
over the network, so there is nothing to wait for. It is here so that ported
25342535
code which checks it keeps working.
25352536

2537+
### `C_CVar.GetCVarBitfield(name, index)` / `C_CVar.SetCVarBitfield(name, index, value)`
2538+
2539+
Read or write one bit of a cvar, so a single cvar can hold many separate
2540+
true/false settings. The index counts from 1.
2541+
2542+
```lua
2543+
RegisterCVar("myAddonFlags", "0")
2544+
2545+
C_CVar.SetCVarBitfield("myAddonFlags", 1, true)
2546+
C_CVar.SetCVarBitfield("myAddonFlags", 5, true)
2547+
GetCVar("myAddonFlags") -- "17", bits 1 and 5
2548+
C_CVar.GetCVarBitfield("myAddonFlags", 5) -- true
2549+
C_CVar.GetCVarBitfield("myAddonFlags", 2) -- false
2550+
```
2551+
2552+
The index may be 1 to 64. `GetCVarBitfield` returns `nil` outside that range,
2553+
or for a name that is not a cvar, so "no such bit" stays apart from "the bit
2554+
is false".
2555+
2556+
`SetCVarBitfield` returns whether it worked. It returns `false` for an index
2557+
outside the range, for a name that is not a cvar, and for a read-only cvar —
2558+
the same cvars [`GetCVarInfo`](#c_cvargetcvarinfoname) reports `isReadOnly`
2559+
for. A successful write saves the cvar the same way `SetCVar` does.
2560+
2561+
Any cvar works, and a cvar you register yourself is the usual way to use
2562+
these. Every bit is independent, so setting one leaves the rest alone.
2563+
25362564
### `C_CVar.GetCVarBool(cvar)`
25372565

25382566
Returns the cvar's value coerced to a boolean, or `nil` if no cvar

src/cvar/Bitfield.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 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 General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `C_CVar.GetCVarBitfield(name, index)` / `C_CVar.SetCVarBitfield(name, index,
15+
// value)` — treat a cvar's numeric value as a bitfield and read or write one
16+
// bit of it, so a single cvar carries many independent booleans. Upstream this
17+
// backs the record of which tutorial popups a player has dismissed: one cvar,
18+
// one bit per popup, persisted by the config machinery for free.
19+
//
20+
// The index is 1-based. Blizzard types it `luaIndex` rather than `number`,
21+
// which is what settles that.
22+
//
23+
// WIDTH. The value is parsed and rewritten as a uint64 in C++, never handed
24+
// through a Lua number, so all 64 bits are exact — Lua 5.0's doubles are not in
25+
// the path. The valid index range is 1..64 because that is the width of the
26+
// type the arithmetic runs in, not a limit chosen here.
27+
//
28+
// Setting refuses, returning false, in the cases Blizzard's own documentation
29+
// names as requirements: an unknown cvar, a read-only one, and an index out of
30+
// range. Both of the first two are things this client can genuinely answer --
31+
// the lookup is the same filtering one behind C_CVar.DoesCVarExist, so a cvar
32+
// preserved out of Config.wtf without being implemented is refused here exactly
33+
// as it is by every other Lua cvar function. Nothing here is ever secure, which
34+
// is the remaining documented requirement.
35+
//
36+
// Writing goes through CVar::Factory::SetString rather than the engine setter
37+
// directly: that wrapper already declares FUN_SET_CVAR_VALUE as the __thiscall
38+
// it is, and getting that wrong corrupts the stack silently rather than
39+
// crashing. It also means a bitfield write fires the cvar's change callback and
40+
// marks the config dirty, exactly as SetCVar would.
41+
42+
#include "Factory.h"
43+
44+
#include "Game.h"
45+
#include "Offsets.h"
46+
47+
#include <cstdint>
48+
#include <cstdio>
49+
50+
namespace CVar::Bitfield {
51+
52+
namespace {
53+
54+
// The arithmetic type's own width, which is what bounds a valid index.
55+
constexpr int kBitCount = 64;
56+
57+
// Parses the cvar's value as an unsigned base-10 integer. A value that is not a
58+
// number reads as 0, which is how an unset bitfield behaves anyway: every bit
59+
// clear. Stops at the first non-digit rather than rejecting, so a value with
60+
// trailing text still yields its leading number.
61+
uint64_t ValueOf(CVar::Factory::Handle cvar) {
62+
const char *s = CVar::Factory::GetString(cvar);
63+
if (s == nullptr)
64+
return 0;
65+
while (*s == ' ' || *s == '\t')
66+
++s;
67+
uint64_t v = 0;
68+
for (; *s >= '0' && *s <= '9'; ++s) {
69+
const uint64_t digit = static_cast<uint64_t>(*s - '0');
70+
if (v > (UINT64_MAX - digit) / 10)
71+
return UINT64_MAX; // saturate rather than wrap
72+
v = v * 10 + digit;
73+
}
74+
return v;
75+
}
76+
77+
// True when `index` names a bit this can address. 1-based.
78+
bool IndexInRange(double index) {
79+
return index >= 1.0 && index <= static_cast<double>(kBitCount) &&
80+
index == static_cast<double>(static_cast<int>(index));
81+
}
82+
83+
// `C_CVar.GetCVarBitfield(name, index) -> value`
84+
//
85+
// nil for an unknown cvar or an index out of range, matching the contract's
86+
// nilable return, so "no such bit" stays distinguishable from "bit is false".
87+
int __fastcall Script_GetCVarBitfield(void *L) {
88+
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
89+
Game::Lua::PushNil(L);
90+
return 1;
91+
}
92+
const double index = Game::Lua::ToNumber(L, 2);
93+
CVar::Factory::Handle cvar = CVar::Factory::Find(Game::Lua::ToString(L, 1));
94+
if (cvar == nullptr || !IndexInRange(index)) {
95+
Game::Lua::PushNil(L);
96+
return 1;
97+
}
98+
const uint64_t bit = uint64_t{1} << (static_cast<int>(index) - 1);
99+
Game::Lua::PushBool(L, (ValueOf(cvar) & bit) != 0);
100+
return 1;
101+
}
102+
103+
// `C_CVar.SetCVarBitfield(name, index, value) -> success`
104+
int __fastcall Script_SetCVarBitfield(void *L) {
105+
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
106+
Game::Lua::PushBool(L, false);
107+
return 1;
108+
}
109+
const double index = Game::Lua::ToNumber(L, 2);
110+
CVar::Factory::Handle cvar = CVar::Factory::Find(Game::Lua::ToString(L, 1));
111+
if (cvar == nullptr || !IndexInRange(index)) {
112+
Game::Lua::PushBool(L, false);
113+
return 1;
114+
}
115+
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
116+
static_cast<const uint8_t *>(cvar) + Offsets::OFF_CVAR_FLAGS);
117+
if ((flags & Offsets::CVAR_FLAG_READ_ONLY) != 0) {
118+
Game::Lua::PushBool(L, false);
119+
return 1;
120+
}
121+
122+
const uint64_t bit = uint64_t{1} << (static_cast<int>(index) - 1);
123+
const uint64_t before = ValueOf(cvar);
124+
const uint64_t after = Game::Lua::ToBoolean(L, 3) != 0 ? (before | bit) : (before & ~bit);
125+
if (after != before) {
126+
char buf[24]; // 20 digits for a uint64 at most, plus the terminator
127+
std::snprintf(buf, sizeof buf, "%llu", static_cast<unsigned long long>(after));
128+
CVar::Factory::SetString(cvar, buf);
129+
}
130+
Game::Lua::PushBool(L, true);
131+
return 1;
132+
}
133+
134+
void Register() {
135+
Game::Lua::RegisterTableFunction("C_CVar", "GetCVarBitfield", &Script_GetCVarBitfield);
136+
Game::Lua::RegisterTableFunction("C_CVar", "SetCVarBitfield", &Script_SetCVarBitfield);
137+
}
138+
139+
const Game::ModuleAutoRegister _autoreg{&Register};
140+
const Game::GlueModuleAutoRegister _glueAutoreg{&Register};
141+
142+
} // namespace
143+
144+
} // namespace CVar::Bitfield

0 commit comments

Comments
 (0)