// Combos use the current levels of the arts they are made of (request of // 2026-10-18). // // A combo (cCreatureArts +0x10e, layout in game/engine/arts.h) keeps the arts // of its steps and a copy of their levels from when it was made at the // Kombomeister; its regeneration total and the steps follow those levels, so // raising an art later does not change the combo. Every engine tick the // hero's combos are compared with the arts' current levels (cooldown entry // level + bonus levels); a combo whose levels are out of date is set again // with the game's own cCreatureArts::SetCombo (0x581290), which recomputes the // total. The charge keeps its fraction of the total. #include #include #include "game/engine/arts.h" #include "game/engine/events.h" #include "game/engine/objects.h" #include "re/log.h" namespace game { // cCreatureArts::SetCombo (0x581290, ret 0x10). using SetComboFn = void(__fastcall*)(void* arts, void* edx, uint32_t index, const uint32_t* stepArts, const uint16_t* levels, uint32_t id); } // namespace game namespace mods::combos { namespace { using namespace game; // combo and cooldown layout (arts.h) // The art's level now (cooldown entry level + bonus levels), 0 if unknown. uint16_t CurrentLevel(uint8_t* arts, uint32_t art) { uint8_t* entry = art ? FindCooldownEntry(arts, static_cast(art)) : nullptr; return entry ? static_cast(entry[kCooldownLevel] + entry[kCooldownBonusLevels]) : 0; } void SyncCombos(uint8_t* hero) { uint8_t* arts = hero + kCreatureArts; if (hero[kCreatureComboStep] != 0 || arts[kArtsComboRunning] != 0) { return; // not while a combo is being executed } for (unsigned index = 0;; ++index) { uint8_t* combo = ComboEntry(arts, index); if (!combo) { return; } const uint16_t id = *reinterpret_cast(combo + kComboId); if (id == 0) { continue; } uint32_t stepArts[kComboSteps]; uint16_t levels[kComboSteps]; std::memcpy(stepArts, combo + kComboArts, sizeof(stepArts)); std::memcpy(levels, combo + kComboLevels, sizeof(levels)); bool changed = false; for (unsigned step = 0; step < kComboSteps; ++step) { const uint16_t level = CurrentLevel(arts, stepArts[step]); if (level != 0 && level != levels[step]) { // unknown arts keep their level levels[step] = level; changed = true; } } if (!changed) { continue; } const float oldTotal = Float(combo, kComboTotal); const float fraction = oldTotal > 0.0f ? Float(combo, kComboCharge) / oldTotal : 1.0f; reinterpret_cast(0x00581290)(arts, nullptr, index, stepArts, levels, id); combo = ComboEntry(arts, index); // SetCombo does not reallocate, but stay safe if (combo) { Float(combo, kComboCharge) = fraction * Float(combo, kComboTotal); re::Log("combos: combo %u set to the current levels %u/%u/%u/%u", index, levels[0], levels[1], levels[2], levels[3]); } } } void OnTick(uint8_t*) { if (auto* hero = static_cast(GetPlayerHero())) { SyncCombos(hero); } } struct Registration { Registration() { OnEngineTick(&OnTick); } } g_registration; } // namespace } // namespace mods::combos