mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 04:17:38 +00:00
feat(cvar): add C_CVar.SetTempCVar and RemoveTempCVar
SetTempCVar changes a cvar for the session without saving it to Config.wtf; RemoveTempCVar restores the value from before the first temp set. The temp set passes a6 = 0 to the engine setter, the only argument that marks the config dirty, so it applies the value and runs the change callback without asking for a save. When something else dirties the config, a co-hook on the config writer (0x0063D980) points each temp cvar's value at its saved string for the write and restores it after. A plain SetCVar over a temp value wins: the entry drops itself, so the user's set is saved and never rolled back. Errors mirror Script_SetCVar. Registered in-game and on glue.
This commit is contained in:
@@ -64,7 +64,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [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` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.SetCVarBitfield` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.RemoveTempCVar`, `C_CVar.SetCVarBitfield`, `C_CVar.SetTempCVar` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [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` |
|
||||
@@ -135,7 +135,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
|-------|-------|
|
||||
| [Account](docs/API.md#account) | `SaveAccount`, `DeleteAccount`, `GetSavedAccounts`, `LoginWithSavedAccount` (passwords encrypted in Windows Credential Manager, scoped per realmlist; plaintext never returned to Lua) |
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
+32
@@ -127,6 +127,7 @@ build instructions.
|
||||
- [`C_CVar.DoesCVarExist(name)`](#c_cvardoescvarexistname)
|
||||
- [`C_CVar.AreCVarsLoaded()`](#c_cvararecvarsloaded)
|
||||
- [`C_CVar.GetCVarBitfield(name, index)` / `C_CVar.SetCVarBitfield(name, index, value)`](#c_cvargetcvarbitfieldname-index--c_cvarsetcvarbitfieldname-index-value)
|
||||
- [`C_CVar.SetTempCVar(name, value)` / `C_CVar.RemoveTempCVar(name)`](#c_cvarsettempcvarname-value--c_cvarremovetempcvarname)
|
||||
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
|
||||
- [The interface memory limit](#the-interface-memory-limit)
|
||||
|
||||
@@ -3243,6 +3244,37 @@ for. A successful write saves the cvar the same way `SetCVar` does.
|
||||
Any cvar works, and a cvar you register yourself is the usual way to use
|
||||
these. Every bit is independent, so setting one leaves the rest alone.
|
||||
|
||||
### `C_CVar.SetTempCVar(name, value)` / `C_CVar.RemoveTempCVar(name)`
|
||||
|
||||
`SetTempCVar` changes a cvar for this session only. The game uses the new
|
||||
value at once, but it does not save the value to Config.wtf.
|
||||
`RemoveTempCVar` puts back the value that the cvar had before the first
|
||||
`SetTempCVar`. Neither function returns a value.
|
||||
|
||||
```lua
|
||||
-- Saved value of cameraDistanceMax is "15".
|
||||
C_CVar.SetTempCVar("cameraDistanceMax", "40")
|
||||
GetCVar("cameraDistanceMax") -- "40"
|
||||
-- After /reload or logout, Config.wtf still holds "15".
|
||||
C_CVar.RemoveTempCVar("cameraDistanceMax")
|
||||
GetCVar("cameraDistanceMax") -- "15"
|
||||
```
|
||||
|
||||
Use this pair for a mode that changes many cvars and later turns off, for
|
||||
example a gamepad mode. The saved settings of the user do not change.
|
||||
|
||||
- If you call `SetTempCVar` two times on one cvar, `RemoveTempCVar` puts
|
||||
back the value from before the first call.
|
||||
- If `SetCVar` changes a cvar that has a temp value, the `SetCVar` value
|
||||
replaces the temp value. The game saves it, and `RemoveTempCVar` then does
|
||||
not change the cvar.
|
||||
- If `value` is `nil`, the cvar gets an empty string, as with `SetCVar`.
|
||||
- `RemoveTempCVar` does nothing for a cvar that has no temp value.
|
||||
|
||||
Both functions give the same errors as `SetCVar`: for a name that is not a
|
||||
cvar, and for a read-only cvar. Both are also available on the login and
|
||||
character-select screens.
|
||||
|
||||
### `C_CVar.GetCVarBool(cvar)`
|
||||
|
||||
Returns the cvar's value coerced to a boolean, or `nil` if no cvar
|
||||
|
||||
@@ -1160,6 +1160,19 @@ enum Offsets {
|
||||
// Where `FUN_0063D380` stores that pointer. Read by the loader call above
|
||||
// and by the save.
|
||||
VAR_CONFIG_FILENAME_PTR = 0x00C4EDD4,
|
||||
// The config writer, `"WTF\" + [VAR_CONFIG_FILENAME_PTR]`. `int(void)`
|
||||
// (decompiled). Returns 1 at once unless VAR_CVAR_CONFIG_DIRTY is set;
|
||||
// otherwise clears it and writes `SET name "value"` for every cvar with
|
||||
// CVAR_FLAG_ARCHIVE, taking the value from OFF_CVAR_STAGED_STR, else
|
||||
// OFF_CVAR_VALUE_STR, else OFF_CVAR_DEFAULT_STR, and skipping any that
|
||||
// equals the default. Called from the /reload + logout teardown
|
||||
// FUN_00490BD0, from FUN_0046B500 (glue), and from the cvar shutdown
|
||||
// FUN_0063DAF0 — cold paths only.
|
||||
FUN_CVAR_CONFIG_WRITE = 0x0063D980,
|
||||
// u8 "config needs saving". Set by the inner setter FUN_0063E0B0 only when
|
||||
// its last argument (a6) is nonzero, and by the staged-value path in
|
||||
// FUN_SET_CVAR_VALUE; cleared by FUN_CVAR_CONFIG_WRITE.
|
||||
VAR_CVAR_CONFIG_DIRTY = 0x00C4EDD8,
|
||||
|
||||
// Internal CVar registrar — what `Script_RegisterCVar` calls after a
|
||||
// `FindCVar` miss (the call at `0x00488B8A`). `__fastcall`; ECX=name,
|
||||
@@ -1192,6 +1205,9 @@ enum Offsets {
|
||||
// when they differ, which is what identifies each; +0x34 is the reset
|
||||
// value it prints the same way, and +0x38 holds a staged value (below).
|
||||
OFF_CVAR_DEFAULT_STR = 0x30,
|
||||
// The staged value (CVAR_FLAG_STAGED below). FUN_CVAR_CONFIG_WRITE prefers
|
||||
// it over the live value when non-null.
|
||||
OFF_CVAR_STAGED_STR = 0x38,
|
||||
// Flag bits within OFF_CVAR_FLAGS, each from the code that acts on it:
|
||||
// 0x1 archive — the registrar forces it on, and the config writer
|
||||
// (FUN_0063D980) skips any cvar without it.
|
||||
|
||||
@@ -91,11 +91,11 @@ int GetInt(Handle cvar, int fallback) {
|
||||
return std::atoi(value);
|
||||
}
|
||||
|
||||
void SetString(Handle cvar, const char *value) {
|
||||
void SetString(Handle cvar, const char *value, bool persist) {
|
||||
if (cvar == nullptr || value == nullptr)
|
||||
return;
|
||||
auto set = reinterpret_cast<SetCVarValue_t>(Offsets::FUN_SET_CVAR_VALUE);
|
||||
set(cvar, value, 1, 0, 0, 1);
|
||||
set(cvar, value, 1, 0, 0, persist ? 1 : 0);
|
||||
}
|
||||
|
||||
void SetInt(Handle cvar, int value) {
|
||||
|
||||
+6
-1
@@ -80,7 +80,12 @@ int GetInt(Handle cvar, int fallback = 0);
|
||||
|
||||
// Set the cvar's value (fires its change callback). No-op if `cvar` is
|
||||
// null. `value` is copied by the engine into the cvar's inline buffer.
|
||||
void SetString(Handle cvar, const char *value);
|
||||
//
|
||||
// `persist` is the setter's last argument, the only thing that marks the
|
||||
// config dirty (FUN_0063E0B0 sets VAR_CVAR_CONFIG_DIRTY only when it is
|
||||
// nonzero). False applies the value for this session without asking the
|
||||
// config writer to run; `CVar::Temp` relies on that.
|
||||
void SetString(Handle cvar, const char *value, bool persist = true);
|
||||
|
||||
// Convenience: set from an integer.
|
||||
//
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_CVar.SetTempCVar(name, value)` / `C_CVar.RemoveTempCVar(name)` — change a
|
||||
// cvar for this session without the change reaching Config.wtf, then put the
|
||||
// saved value back. Upstream's gamepad mode flips two dozen camera and
|
||||
// soft-target cvars this way so the user's own settings survive it.
|
||||
//
|
||||
// Two engine facts make this nearly free:
|
||||
//
|
||||
// 1. The setter only marks the config dirty when its last argument is
|
||||
// nonzero (FUN_0063E0B0: `if (a6) VAR_CVAR_CONFIG_DIRTY = 1`). A temp set
|
||||
// passes 0, so it applies the value and fires the change callback exactly
|
||||
// as SetCVar does, but never asks the config writer to run.
|
||||
// 2. The config writer (FUN_CVAR_CONFIG_WRITE) reads each cvar's live value
|
||||
// pointer and nothing else of ours. When something ELSE dirtied the config
|
||||
// and the writer does run, a co-hook points each temp cvar's value back at
|
||||
// its saved string for the duration of the write and restores it after. So
|
||||
// the file gets exactly the line the engine would have written had the
|
||||
// temp value never been set — including no line at all when the saved
|
||||
// value is the default.
|
||||
//
|
||||
// A plain SetCVar on a temp cvar wins: the entry notices the live value is no
|
||||
// longer the one it set and drops itself, both in the writer hook and in
|
||||
// RemoveTempCVar, so the user's own set is saved and never rolled back.
|
||||
//
|
||||
// Validation mirrors Script_SetCVar (FUN_00488C10) word for word: a usage
|
||||
// error, `Couldn't find CVar named '%s'`, and `"%s" is read only`. The lookup
|
||||
// is FUN_FIND_CVAR, so a cvar Lua cannot see is refused as unknown, which is
|
||||
// the contract's "valid and public". Nothing here is secure, the remaining
|
||||
// documented requirement. A nil value becomes "", as it does for SetCVar.
|
||||
|
||||
#include "Factory.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace CVar::Temp {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Entry {
|
||||
CVar::Factory::Handle cvar;
|
||||
std::string saved; // the value before the first temp set
|
||||
std::string temp; // the value that landed (after the change callback)
|
||||
};
|
||||
|
||||
std::vector<Entry> g_entries;
|
||||
|
||||
Entry *FindEntry(CVar::Factory::Handle cvar) {
|
||||
for (Entry &e : g_entries)
|
||||
if (e.cvar == cvar)
|
||||
return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void EraseEntry(const Entry *entry) {
|
||||
g_entries.erase(g_entries.begin() + (entry - g_entries.data()));
|
||||
}
|
||||
|
||||
const char *LiveValue(CVar::Factory::Handle cvar) {
|
||||
const char *value = CVar::Factory::GetString(cvar);
|
||||
return value != nullptr ? value : "";
|
||||
}
|
||||
|
||||
// True while the live value is still the one SetTempCVar put there.
|
||||
bool StillTemp(const Entry &e) {
|
||||
return e.temp == LiveValue(e.cvar);
|
||||
}
|
||||
|
||||
const char **ValueSlot(CVar::Factory::Handle cvar) {
|
||||
return reinterpret_cast<const char **>(static_cast<uint8_t *>(cvar) +
|
||||
Offsets::OFF_CVAR_VALUE_STR);
|
||||
}
|
||||
|
||||
// Script_SetCVar's own lookup and checks, with its error text.
|
||||
CVar::Factory::Handle Resolve(void *L, const char *usage) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "%s", usage);
|
||||
return nullptr;
|
||||
}
|
||||
const char *name = Game::Lua::ToString(L, 1);
|
||||
CVar::Factory::Handle cvar = CVar::Factory::Find(name);
|
||||
if (cvar == nullptr) {
|
||||
Game::Lua::Error(L, "Couldn't find CVar named '%s'", name);
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<const uint8_t *>(cvar) + Offsets::OFF_CVAR_FLAGS);
|
||||
if ((flags & Offsets::CVAR_FLAG_READ_ONLY) != 0) {
|
||||
Game::Lua::Error(L, "\"%s\" is read only", name);
|
||||
return nullptr;
|
||||
}
|
||||
return cvar;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTempCVar(void *L) {
|
||||
CVar::Factory::Handle cvar =
|
||||
Resolve(L, "Usage: C_CVar.SetTempCVar(\"cvar\", value)");
|
||||
if (cvar == nullptr)
|
||||
return 0;
|
||||
const char *value = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "";
|
||||
|
||||
Entry *entry = FindEntry(cvar);
|
||||
if (entry != nullptr && !StillTemp(*entry)) {
|
||||
// A plain SetCVar replaced the temp value; that is the saved value now.
|
||||
entry->saved = LiveValue(cvar);
|
||||
} else if (entry == nullptr) {
|
||||
g_entries.push_back({cvar, LiveValue(cvar), {}});
|
||||
entry = &g_entries.back();
|
||||
}
|
||||
CVar::Factory::SetString(cvar, value, /*persist*/ false);
|
||||
// Record what landed, not what was asked: a change callback may clamp.
|
||||
// Re-find rather than reuse `entry`, since the callback runs Lua.
|
||||
if (Entry *landed = FindEntry(cvar))
|
||||
landed->temp = LiveValue(cvar);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_RemoveTempCVar(void *L) {
|
||||
CVar::Factory::Handle cvar =
|
||||
Resolve(L, "Usage: C_CVar.RemoveTempCVar(\"cvar\")");
|
||||
if (cvar == nullptr)
|
||||
return 0;
|
||||
Entry *entry = FindEntry(cvar);
|
||||
if (entry == nullptr)
|
||||
return 0;
|
||||
if (StillTemp(*entry)) {
|
||||
// Copy first: the setter's change callback could, in principle, reach
|
||||
// back in here and move the vector.
|
||||
const std::string saved = entry->saved;
|
||||
EraseEntry(entry);
|
||||
CVar::Factory::SetString(cvar, saved.c_str(), /*persist*/ false);
|
||||
} else {
|
||||
EraseEntry(entry);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Config writer co-hook ------------------------------------------------
|
||||
|
||||
using ConfigWrite_t = int(__cdecl *)();
|
||||
ConfigWrite_t s_configWrite_o = nullptr;
|
||||
|
||||
int __cdecl ConfigWrite_h() {
|
||||
// Only a dirty config gets written; skip the swap work otherwise.
|
||||
if (g_entries.empty() ||
|
||||
*reinterpret_cast<const uint8_t *>(Offsets::VAR_CVAR_CONFIG_DIRTY) == 0)
|
||||
return s_configWrite_o();
|
||||
|
||||
for (size_t i = g_entries.size(); i-- > 0;)
|
||||
if (!StillTemp(g_entries[i]))
|
||||
g_entries.erase(g_entries.begin() + i);
|
||||
|
||||
std::vector<const char *> live(g_entries.size());
|
||||
for (size_t i = 0; i < g_entries.size(); ++i) {
|
||||
const char **slot = ValueSlot(g_entries[i].cvar);
|
||||
live[i] = *slot;
|
||||
*slot = g_entries[i].saved.c_str();
|
||||
}
|
||||
const int result = s_configWrite_o();
|
||||
for (size_t i = 0; i < g_entries.size(); ++i)
|
||||
*ValueSlot(g_entries[i].cvar) = live[i];
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookConfigWrite{
|
||||
Offsets::FUN_CVAR_CONFIG_WRITE,
|
||||
reinterpret_cast<void *>(&ConfigWrite_h),
|
||||
reinterpret_cast<void **>(&s_configWrite_o)};
|
||||
|
||||
// --- Registration ---------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSetArgs[] = {
|
||||
Game::Doc::Req("name", "cstring"),
|
||||
Game::Doc::Opt("value", "cstring"),
|
||||
};
|
||||
const Game::Doc::Function kSetTempCVar{
|
||||
"Sets a console variable for this session only; the change is not saved.",
|
||||
kSetArgs, {}};
|
||||
|
||||
const Game::Doc::Field kRemoveArgs[] = {
|
||||
Game::Doc::Req("name", "cstring"),
|
||||
};
|
||||
const Game::Doc::Function kRemoveTempCVar{
|
||||
"Restores the value a console variable had before SetTempCVar.",
|
||||
kRemoveArgs, {}};
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "SetTempCVar", &Script_SetTempCVar,
|
||||
&kSetTempCVar);
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "RemoveTempCVar", &Script_RemoveTempCVar,
|
||||
&kRemoveTempCVar);
|
||||
}
|
||||
|
||||
// Cvar storage is process-global, so the same surface works pre-login.
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CVar::Temp
|
||||
Reference in New Issue
Block a user