Skip to content

Instantly share code, notes, and snippets.

@wilrodriguez
Created July 11, 2026 17:59
Show Gist options
  • Select an option

  • Save wilrodriguez/d76f59ea6a4c6e7ad4b2a2a9af7d4fdf to your computer and use it in GitHub Desktop.

Select an option

Save wilrodriguez/d76f59ea6a4c6e7ad4b2a2a9af7d4fdf to your computer and use it in GitHub Desktop.
Allow KCD Cheat Xtended to accept string literals

CheatXtended Exact Match & Quote Literal Patch

This patch adds support for exact matches to the CheatXtended mod console commands. It resolves ambiguities when two NPC or faction names are sub-strings of one another (e.g., rat_tailor vs. rat_tailors_wife).

NOTE: I generated this patch with AI, I don't know if it works, and it probably should be reviewed by a human who knows C++ better than I do. I haven't touched C++ since college, and I wasn't very good at it then either.

What this patch does:

  1. Prioritizes Exact Matches: When you run a command (like cheatx_set_opinion), the mod first checks if there is a case-insensitive exact match for the name you typed. If it finds one, it immediately targets that NPC/faction and ignores any other substring matches, resolving the ambiguity automatically without needing quotes.
  2. Quote Handling (CleanQuery): If you pass an argument wrapped in single (') or double (") quotes, the mod cleans the quotes and forces an exact-match check (disabling the fallback to substring matching entirely). This allows you to enforce literal matches.
  3. Applies to all target matching commands:
    • cheatx_set_reputation
    • cheatx_set_opinion
    • cheatx_clear_witness
    • Stats and skills lookup (cheatx_set_stat, cheatx_set_skill, cheatx_set_perk_points)

The Patch File

You can access and copy the patch file directly from your disk here: cheat_xtended_exact_match.patch

--- a/Projects/CheatXtended/src/plugin.cpp
+++ b/Projects/CheatXtended/src/plugin.cpp
@@ -39,6 +39,7 @@
#include <cstdint>
#include <cctype>
#include <vector>
+#include <string>
using wh::xgenaimodule::C_InformationManager;
using wh::xgenaimodule::S_InformationRecord;
@@ -52,6 +53,19 @@
using wh::rpgmodule::E_RPGSkill;
namespace {
+
+// Helper to strip quotes and force exact matching
+std::string CleanQuery(const char* arg, bool& outForceExact)
+{
+ outForceExact = false;
+ if (!arg) return "";
+ std::string s(arg);
+ if (s.length() >= 2 && ((s.front() == '"' && s.back() == '"') || (s.front() == ''' && s.back() == '''))) {
+ s = s.substr(1, s.length() - 2);
+ outForceExact = true;
+ }
+ return s;
+}
// ---------------------------------------------------------------------------
// Arg parsing
@@ -238,9 +252,21 @@
// Find by substring match. Returns match count; fills outId on single match.
int FindStat(const char* query, int& outId)
{
+ bool forceExact = false;
+ std::string clean = CleanQuery(query, forceExact);
+ const char* q = clean.c_str();
+
+ for (int i = 0; i < kStatCount; ++i) {
+ if (_stricmp(kStats[i].shortName, q) == 0 || _stricmp(kStats[i].longName, q) == 0) {
+ outId = kStats[i].id;
+ return 1;
+ }
+ }
+ if (forceExact) return 0;
+
int count = 0;
for (int i = 0; i < kStatCount; ++i) {
- if (MatchesName(kStats[i].shortName, query) || MatchesName(kStats[i].longName, query)) {
+ if (MatchesName(kStats[i].shortName, q) || MatchesName(kStats[i].longName, q)) {
outId = kStats[i].id;
++count;
}
@@ -250,9 +276,21 @@
int FindSkill(const char* query, int& outId)
{
+ bool forceExact = false;
+ std::string clean = CleanQuery(query, forceExact);
+ const char* q = clean.c_str();
+
+ for (int i = 0; i < kSkillCount; ++i) {
+ if (_stricmp(kSkills[i].shortName, q) == 0 || _stricmp(kSkills[i].longName, q) == 0) {
+ outId = kSkills[i].id;
+ return 1;
+ }
+ }
+ if (forceExact) return 0;
+
int count = 0;
for (int i = 0; i < kSkillCount; ++i) {
- if (MatchesName(kSkills[i].shortName, query) || MatchesName(kSkills[i].longName, query)) {
+ if (MatchesName(kSkills[i].shortName, q) || MatchesName(kSkills[i].longName, q)) {
outId = kSkills[i].id;
++count;
}
@@ -262,16 +300,20 @@
void ListStatMatches(Offsets::ILog* log, const char* query)
{
+ bool forceExact = false;
+ std::string clean = CleanQuery(query, forceExact);
for (int i = 0; i < kStatCount; ++i) {
- if (MatchesName(kStats[i].shortName, query) || MatchesName(kStats[i].longName, query))
+ if (MatchesName(kStats[i].shortName, clean.c_str()) || MatchesName(kStats[i].longName, clean.c_str()))
log->LogAlways("[CheatX] '%s' (%s)", kStats[i].shortName, kStats[i].longName);
}
}
void ListSkillMatches(Offsets::ILog* log, const char* query)
{
+ bool forceExact = false;
+ std::string clean = CleanQuery(query, forceExact);
for (int i = 0; i < kSkillCount; ++i) {
- if (MatchesName(kSkills[i].shortName, query) || MatchesName(kSkills[i].longName, query))
+ if (MatchesName(kSkills[i].shortName, clean.c_str()) || MatchesName(kSkills[i].longName, clean.c_str()))
log->LogAlways("[CheatX] '%s' (%s)", kSkills[i].shortName, kSkills[i].longName);
}
}
@@ -408,28 +450,42 @@
{
auto* log = SSystemGlobalEnvironment::GetInstance()->pLog;
if (!log) return;
- const char* name = ArgString(args, 1);
- if (!name) { log->LogAlways("[CheatX] usage: cheatx_set_reputation <faction_name> <value>"); return; }
+ const char* nameArg = ArgString(args, 1);
+ if (!nameArg) { log->LogAlways("[CheatX] usage: cheatx_set_reputation <faction_name> <value>"); return; }
float value = ArgFloat(args, 2, 0.0f);
if (value < -1.0f) value = -1.0f; else if (value > 1.0f) value = 1.0f;
auto* mgr = C_FactionManager::GetInstance();
if (!mgr) { log->LogAlways("[CheatX] no faction manager"); return; }
+ bool forceExact = false;
+ std::string cleanName = CleanQuery(nameArg, forceExact);
+ const char* name = cleanName.c_str();
+
std::vector<C_Faction*> matches;
+ C_Faction* exactMatch = nullptr;
for (const auto& kv : mgr->m_factions) {
C_Faction* f = kv.second.get();
if (!f || !f->m_pDef) continue;
- if (MatchesName(f->m_pDef->name.c_str(), name))
+ if (_stricmp(f->m_pDef->name.c_str(), name) == 0) {
+ exactMatch = f;
+ break;
+ }
+ if (!forceExact && MatchesName(f->m_pDef->name.c_str(), name))
matches.push_back(f);
}
+ if (exactMatch) {
+ matches.clear();
+ matches.push_back(exactMatch);
+ }
+
if (matches.empty()) {
- log->LogAlways("[CheatX] no faction matching '%s'", name);
+ log->LogAlways("[CheatX] no faction matching '%s'", nameArg);
return;
}
if (matches.size() > 1) {
- log->LogAlways("[CheatX] ambiguous -- %d factions match '%s':", static_cast<int>(matches.size()), name);
+ log->LogAlways("[CheatX] ambiguous -- %d factions match '%s':", static_cast<int>(matches.size()), nameArg);
for (C_Faction* f : matches)
log->LogAlways("[CheatX] '%s' (rep=%.2f)", f->m_pDef->name.c_str(), f->m_reputation);
return;
@@ -445,24 +501,40 @@
{
auto* log = SSystemGlobalEnvironment::GetInstance()->pLog;
if (!log) return;
- const char* name = ArgString(args, 1);
- if (!name) { log->LogAlways("[CheatX] usage: cheatx_set_opinion <npc_name> <value>"); return; }
+ const char* nameArg = ArgString(args, 1);
+ if (!nameArg) { log->LogAlways("[CheatX] usage: cheatx_set_opinion <npc_name> <value>"); return; }
float value = ArgFloat(args, 2, 0.0f);
if (value < -1.0f) value = -1.0f; else if (value > 1.0f) value = 1.0f;
+ bool forceExact = false;
+ std::string cleanName = CleanQuery(nameArg, forceExact);
+ const char* name = cleanName.c_str();
+
struct Match { C_Soul* soul; float dist; };
std::vector<Match> matches;
+ Match exactMatch = {nullptr, 0.0f};
+ bool foundExact = false;
+
ForEachNpc(0.0f, [&](C_Soul* s, float dist) {
- if (MatchesName(s->m_soulName.c_str(), name))
+ if (_stricmp(s->m_soulName.c_str(), name) == 0) {
+ exactMatch = {s, dist};
+ foundExact = true;
+ }
+ if (!forceExact && MatchesName(s->m_soulName.c_str(), name))
matches.push_back({s, dist});
});
+ if (foundExact) {
+ matches.clear();
+ matches.push_back(exactMatch);
+ }
+
if (matches.empty()) {
- log->LogAlways("[CheatX] no loaded NPC matching '%s'", name);
+ log->LogAlways("[CheatX] no loaded NPC matching '%s'", nameArg);
return;
}
if (matches.size() > 1) {
- log->LogAlways("[CheatX] ambiguous -- %d NPCs match '%s':", static_cast<int>(matches.size()), name);
+ log->LogAlways("[CheatX] ambiguous -- %d NPCs match '%s':", static_cast<int>(matches.size()), nameArg);
for (auto& m : matches)
log->LogAlways("[CheatX] '%s' (opinion=%.2f, dist=%.1fm)", m.soul->m_soulName.c_str(), m.soul->GetPlayerOpinion(), m.dist);
return;
@@ -655,12 +727,28 @@
return;
}
+ bool forceExact = false;
+ std::string cleanArg = CleanQuery(arg, forceExact);
+ const char* name = cleanArg.c_str();
+
struct Match { C_Soul* soul; float dist; };
std::vector<Match> matches;
+ Match exactMatch = {nullptr, 0.0f};
+ bool foundExact = false;
+
ForEachNpc(0.0f, [&](C_Soul* s, float dist) {
- if (MatchesName(s->m_soulName.c_str(), arg))
+ if (_stricmp(s->m_soulName.c_str(), name) == 0) {
+ exactMatch = {s, dist};
+ foundExact = true;
+ }
+ if (!forceExact && MatchesName(s->m_soulName.c_str(), name))
matches.push_back({s, dist});
});
+
+ if (foundExact) {
+ matches.clear();
+ matches.push_back(exactMatch);
+ }
if (matches.empty()) {
log->LogAlways("[CheatX] no loaded NPC matching '%s'", arg);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment